Конкурс Популярности - Картина По Номерам

  • Автор темы Sportdjanton
  • Обновлено
  • 22, Oct 2024
  • #1

Вам предоставляется настоящее цветное изображение. Ваша задача — создать версию этого изображения, которая выглядела бы так, как будто она была нарисована с помощью раскраска по номерам (детская деятельность, нет нонограммы). Помимо изображения вам даются два параметра: П, максимальный размер цветовой палитры (т. е. максимальное количество различных цветов, которые можно использовать) и Н, максимальное количество клетки использовать. Ваш алгоритм делает нет придется использовать все П цвета и Н ячеек, но он не должен использовать больше этого. Выходное изображение должно иметь те же размеры, что и входное.

А клетка определяется как непрерывная область пикселей, имеющих одинаковый цвет. Пиксели, соприкасающиеся только в углах, нет считается смежным. Ячейки могут иметь отверстия.

Короче говоря, вам нужно аппроксимировать входное изображение только с помощью N областей с плоской заливкой/сплошным цветом и P различных цветов.

Чтобы визуализировать параметры, приведу очень простой пример (без конкретного входного изображения; демонстрирую свои безумные навыки рисования). Следующее изображение имеет Р = 6 и Н = 11:

конкурс популярности - Картина по номерам

Вот несколько изображений для проверки вашего алгоритма (в основном это наши обычные подозреваемые). Нажмите на картинку, чтобы увидеть увеличенные версии.

конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам конкурс популярности - Картина по номерам

Пожалуйста, включите несколько результатов по различным параметрам. Если вы хотите показать большое количество результатов, вы можете создать галерею на imgur.com, чтобы размер ответов был разумным. Альтернативно, поместите миниатюры в свой пост и сделайте их ссылками на более крупные изображения, как я сделал выше. Также не стесняйтесь использовать другие тестовые изображения, если найдете что-то интересное.

Я предполагаю, что параметры вокруг Н ≥ 500, П ~ 30 будет похоже на настоящие шаблоны для рисования по номерам.

Это конкурс популярности, поэтому побеждает ответ, набравший наибольшее количество голосов. Избирателям предлагается оценивать ответы по

  • насколько хорошо аппроксимируются исходные изображения.
  • насколько хорошо алгоритм работает с различными типами изображений (картины, вероятно, в целом проще, чем фотографии).
  • насколько хорошо алгоритм работает с очень ограничительными параметрами.
  • насколько органично/гладко выглядят формы ячеек.

Для проверки результатов я буду использовать следующий скрипт Mathematica:

 image = <pastedimagehere> // ImageData;
palette = Union[Join @@ image];
Print["P = ", Length@palette];
grid = GridGraph[Reverse@Most@Dimensions@image];
image = Flatten[image /. Thread[palette -> Range@Length@palette]];
Print["N = ", 

Length@ConnectedComponents[

Graph[Cases[EdgeList[grid], 

m_ <-> n_ /; image[[m]] == image[[n]]]]]]
 

Sp3000 был настолько любезен, что написал верификатор на Python 2 с использованием PIL, который вы найдете в этом пастербине.

#конкурс популярности #графический вывод #обработка изображений

Sportdjanton


Рег
18 Apr, 2011

Тем
85

Постов
162

Баллов
597
  • 26, Oct 2024
  • #2

Python 2 с PIL (Галерея)

 
 
 
 
 
 
 
 
 
 GRAPHICAL_LOGGING 

Время обновления! В этом обновлении реализован простой алгоритм сглаживания, благодаря которому изображения выглядят менее размытыми. Если я обновлю снова, мне придется переделать значительную часть моего кода, потому что он становится беспорядочным, и я hd 2 glf a fw thngs 2 mke t char lim.

Я также создал цвета веса k-средних на основе размеров ячеек, что теряет некоторые детали для более ограничительных параметров (например, центр туманности и вилы в американской готике), но делает общий выбор цвета более четким и приятным. Интересно, что при P = 5 он теряет весь фон для сфер с трассировкой лучей.

Краткое описание алгоритма:

  1. Преобразуйте пиксели в Цветовое пространство CIELAB: CIELAB лучше соответствует человеческому зрению, чем RGB. Первоначально я использовал ХСЛ (оттенок, насыщенность, яркость), но здесь было две проблемы: оттенок белого/серого/черного не определен, а оттенок измеряется в градусах, которые округляются, что затрудняет использование k-средних.
  2. Разделите изображение на ячейки одинакового цвета, используя заливку: Выберите пиксель вне ячейки и выполните заливку заливкой, используя указанный допуск. Для измерения расстояния между двумя цветами я использую стандартную евклидову норму. Более сложные формулы доступны на эта вики-статья.
  3. Объедините маленькие ячейки со своими соседями: заливка генерирует множество ячеек из 1 или 2 пикселей — объединяйте ячейки меньше указанного размера с соседней ячейкой с наибольшим количеством соседних пикселей. Это значительно уменьшает количество ячеек, сокращая время выполнения последующих шагов.
  4. Объедините регионы одинакового цвета.: пройтись по ячейкам в порядке убывания размера. Если какая-либо соседняя ячейка имеет средний цвет на расстоянии менее определенного расстояния, объедините ячейки. Продолжайте проходить через ячейки, пока больше нельзя будет объединить.
  5. Объединяем до тех пор, пока у нас не будет менее 1,5N ячеек (N-слияние): Объединить ячейки вместе, используя оценку, основанную на размере ячейки и разнице цвета, пока у нас не будет не более 1,5N ячеек. Мы допускаем некоторую свободу действий, так как позже мы снова объединимся.
  6. Объединяем до тех пор, пока у нас не будет меньше P цветов, используя k-средние (P-слияние): используйте алгоритм кластеризации k-средних определенное количество раз для создания кластеров цветов ячеек, вес которых зависит от размера ячейки. Оцените каждую кластеризацию на основе вариации индекса Дэвиса-Булдина и выберите лучшую кластеризацию для использования.
  7. Приблизительное сглаживание по Гауссу: используйте несколько линейных размытий для аппроксимации размытия по Гауссу (подробности здесь). Затем для каждого пикселя выберите цвет самого себя и его соседей в предварительно размытом изображении, наиболее близкий к его цвету в размытом изображении. При необходимости эту часть можно оптимизировать по времени, поскольку мне еще предстоит реализовать оптимальный алгоритм.
  8. Сделайте еще один проход заливки, чтобы проработать новые регионы.: Это необходимо, так как предыдущий шаг может фактически создать более клетки.
  9. Сделайте еще один проход слияния небольших ячеек
  10. Сделайте еще один проход N-слияния: На этот раз мы перейдем к N ячейкам, а не к 1,5N.

Время обработки каждого изображения сильно зависит от его размера и сложности: для тестовых изображений оно составляет от 20 секунд до 7 минут.

Поскольку алгоритм использует рандомизацию (например, слияние, k-средние), вы можете получить разные результаты при разных прогонах. Вот сравнение двух прогонов изображения медведя с N = 50 и P = 10:


Примечание. Все изображения ниже являются ссылками. Большинство этих изображений созданы прямо с первого запуска, но если мне не понравился результат, я позволил себе до трех попыток, чтобы быть честным.

Н = 50, Р = 10

Н = 500, Р = 30

Но я довольно ленив, когда дело доходит до рисования красками, так что просто для развлечения...

Н = 20, Р = 5

Кроме того, забавно видеть, что происходит, когда вы пытаетесь втиснуть 1 миллион цветов в N = 500, P = 30:

Вот пошаговое описание алгоритма подводного изображения с N = 500 и P = 30 в анимированном формате GIF:


Я также сделал галерею для предыдущих версий алгоритма. здесь. Вот некоторые из моих любимых из последней версии (когда в туманности было больше звезд, а медведь выглядел более пушистым):

 

DrulyaEr


Рег
15 Oct, 2004

Тем
69

Постов
193

Баллов
558
  • 26, Oct 2024
  • #3

Python 2 с PIL

Также решение Python и, вероятно, еще очень много работает:

N

Алгоритм использует другой подход, чем в SP3000, и сначала начинает с цветов:

  • Найдите цветовую палитру П цвета по кластеризация k-средних и нарисуйте изображение в этой уменьшенной палитре.

  • Примените небольшой медианный фильтр, чтобы избавиться от шума.

  • Составьте список всех одноцветных клеток и отсортируйте его по размеру.

  • Объединяйте самые маленькие ячейки с их соответствующими самыми большими соседями, пока не останется только Н клетки остались.

Существует немало возможностей для улучшения, как с точки зрения скорости, так и качества результатов. В частности, этап слияния ячеек может занять до нескольких минут и дать далеко не оптимальные результаты.


П = 5, Н = 45

П = 10, Н = 50

П = 4, Н = 250

П = 11, Н = 500

 

DeC


Рег
11 Jun, 2004

Тем
60

Постов
186

Баллов
496
  • 26, Oct 2024
  • #4

Математика

На данный момент для этого требуется количество цветов и радиус Гаусса, которые будут использоваться в фильтре Гаусса. Чем больше радиус, тем сильнее размытие и слияние цветов.

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

Выходные данные включают количество ячеек для каждого цвета, а также общее количество ячеек.

P

Обновлять

N allows to specify the desired number of cells as input. It determines the a best Gaussian Radius by looping through scenarios with greater radii until a close match is found.

P outputs (picture, requested cells, used cells, error, gaussian Radius used).

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

from PIL import Image, ImageFilter from math import sqrt from copy import copy from random import shuffle, choice, seed IN_FILE = "input.png" OUT_FILE = "output.png" LOGGING = True GRAPHICAL_LOGGING = False LOG_FILE_PREFIX = "out" LOG_FILE_SUFFIX = ".png" LOG_ROUND_INTERVAL = 150 LOG_FLIP_INTERVAL = 40000 N = 500 P = 30 BLUR_RADIUS = 3 FILAMENT_ROUND_INTERVAL = 5 seed(0) # Random seed print("Opening input file...") image = Image.open(IN_FILE).filter(ImageFilter.GaussianBlur(BLUR_RADIUS)) pixels = {} width, height = image.size for i in range(width): for j in range(height): pixels[(i, j)] = image.getpixel((i, j)) def dist_rgb((a,b,c), (d,e,f)): return (a-d)**2 + (b-e)**2 + (c-f)**2 def nbors((x,y)): if 0 < x: if 0 < y: yield (x-1,y-1) if y < height-1: yield (x-1,y+1) if x < width - 1: if 0 < y: yield (x+1,y-1) if y < height-1: yield (x+1,y+1) def full_circ((x,y)): return ((x+1,y), (x+1,y+1), (x,y+1), (x-1,y+1), (x-1,y), (x-1,y-1), (x,y-1), (x+1,y-1)) class Region: def __init__(self): self.points = set() self.size = 0 self.sum = (0,0,0) def flip_point(self, point): sum_r, sum_g, sum_b = self.sum r, g, b = pixels[point] if point in self.points: self.sum = (sum_r - r, sum_g - g, sum_b - b) self.size -= 1 self.points.remove(point) else: self.sum = (sum_r + r, sum_g + g, sum_b + b) self.size += 1 self.points.add(point) def mean_with(self, color): if color is None: s = float(self.size) r, g, b = self.sum else: s = float(self.size + 1) r, g, b = map(lambda a,b: a+b, self.sum, color) return (r/s, g/s, b/s) print("Initializing regions...") aspect_ratio = width / float(height) a = int(sqrt(N)*aspect_ratio) b = int(sqrt(N)/aspect_ratio) num_components = a*b owners = {} regions = [Region() for i in range(P)] borders = set() nodes = [(i,j) for i in range(a) for j in range(b)] shuffle(nodes) node_values = {(i,j):None for i in range(a) for j in range(b)} for i in range(P): node_values[nodes[i]] = regions[i] for (i,j) in nodes[P:]: forbiddens = set() for node in (i,j-1), (i,j+1), (i-1,j), (i+1,j): if node in node_values and node_values[node] is not None: forbiddens.add(node_values[node]) node_values[(i,j)] = choice(list(set(regions) - forbiddens)) for (i,j) in nodes: for x in range((width*i)/a, (width*(i+1))/a): for y in range((height*j)/b, (height*(j+1))/b): owner = node_values[(i,j)] owner.flip_point((x,y)) owners[(x,y)] = owner def recalc_borders(point = None): global borders if point is None: borders = set() for i in range(width): for j in range(height): if (i,j) not in borders: owner = owner_of((i,j)) for pt in nbors((i,j)): if owner_of(pt) != owner: borders.add((i,j)) borders.add(pt) break else: for pt in nbors(point): owner = owner_of(pt) for pt2 in nbors(pt): if owner_of(pt2) != owner: borders.add(pt) break else: borders.discard(pt) def owner_of(point): if 0 <= point[0] < width and 0 <= point[1] < height: return owners[point] else: return None # Status codes for analysis SINGLETON = 0 FILAMENT = 1 SWAPPABLE = 2 NOT_SWAPPABLE = 3 def analyze_nbors(point): owner = owner_of(point) circ = a,b,c,d,e,f,g,h = full_circ(point) oa,ob,oc,od,oe,of,og,oh = map(owner_of, circ) nbor_owners = set([oa,oc,oe,og]) if owner not in nbor_owners: return SINGLETON, owner, nbor_owners - set([None]) if oc != oe == owner == oa != og != oc: return FILAMENT, owner, set([og, oc]) - set([None]) if oe != oc == owner == og != oa != oe: return FILAMENT, owner, set([oe, oa]) - set([None]) last_owner = oa flips = {last_owner:0} for (corner, side, corner_owner, side_owner) in (b,c,ob,oc), (d,e,od,oe), (f,g,of,og), (h,a,oh,oa): if side_owner not in flips: flips[side_owner] = 0 if side_owner != corner_owner or side_owner != last_owner: flips[side_owner] += 1 flips[last_owner] += 1 last_owner = side_owner candidates = set(own for own in flips if flips[own] == 2 and own is not None) if owner in candidates: return SWAPPABLE, owner, candidates - set([owner]) return NOT_SWAPPABLE, None, None print("Calculating borders...") recalc_borders() print("Deforming regions...") def assign_colors(): used_colors = {} for region in regions: r, g, b = region.mean_with(None) r, g, b = int(round(r)), int(round(g)), int(round(b)) if (r,g,b) in used_colors: for color in sorted([(r2, g2, b2) for r2 in range(256) for g2 in range(256) for b2 in range(256)], key=lambda color: dist_rgb(color, (r,g,b))): if color not in used_colors: used_colors[color] = region.points break else: used_colors[(r,g,b)] = region.points return used_colors def make_image(colors): img = Image.new("RGB", image.size) for color in colors: for point in colors[color]: img.putpixel(point, color) return img # Round status labels FULL_ROUND = 0 NEIGHBOR_ROUND = 1 FILAMENT_ROUND = 2 max_filament = None next_search = set() rounds = 0 points_flipped = 0 singletons = 0 filaments = 0 flip_milestone = 0 logs = 0 while True: if LOGGING and (rounds % LOG_ROUND_INTERVAL == 0 or points_flipped >= flip_milestone): print("Round %d of deformation:\n %d edit(s) so far, of which %d singleton removal(s) and %d filament cut(s)."%(rounds, points_flipped, singletons, filaments)) while points_flipped >= flip_milestone: flip_milestone += LOG_FLIP_INTERVAL if GRAPHICAL_LOGGING: make_image(assign_colors()).save(LOG_FILE_PREFIX + str(logs) + LOG_FILE_SUFFIX) logs += 1 if max_filament is None or (round_status == NEIGHBOR_ROUND and rounds%FILAMENT_ROUND_INTERVAL != 0): search_space, round_status = (next_search & borders, NEIGHBOR_ROUND) if next_search else (copy(borders), FULL_ROUND) next_search = set() max_filament = None else: round_status = FILAMENT_ROUND search_space = set([max_filament[0]]) & borders search_space = list(search_space) shuffle(search_space) for point in search_space: status, owner, takers = analyze_nbors(point) if (status == FILAMENT and num_components < N) or status in (SINGLETON, SWAPPABLE): color = pixels[point] takers_list = list(takers) shuffle(takers_list) for taker in takers_list: dist = dist_rgb(color, owner.mean_with(None)) - dist_rgb(color, taker.mean_with(color)) if dist > 0: if status != FILAMENT or round_status == FILAMENT_ROUND: found = True owner.flip_point(point) taker.flip_point(point) owners[point] = taker recalc_borders(point) next_search.add(point) for nbor in full_circ(point): next_search.add(nbor) points_flipped += 1 if status == FILAMENT: if round_status == FILAMENT_ROUND: num_components += 1 filaments += 1 elif max_filament is None or max_filament[1] < dist: max_filament = (point, dist) if status == SINGLETON: num_components -= 1 singletons += 1 break rounds += 1 if round_status == FILAMENT_ROUND: max_filament = None if round_status == FULL_ROUND and max_filament is None and not next_search: break print("Deformation completed after %d rounds:\n %d edit(s), of which %d singleton removal(s) and %d filament cut(s)."%(rounds, points_flipped, singletons, filaments)) print("Assigning colors...") used_colors = assign_colors() print("Producing output...") make_image(used_colors).save(OUT_FILE) print("Done!")

Пример, для которого мы указываем количество ячеек, желаемое в выводе.

Пример запроса 90 ячеек по 25 цветов. Решение возвращает 88 ячеек, ошибка 2%. Функция выбрала гауссов радиус 55. (Много искажений).


Примеры, для которых входные данные включают радиус по Гауссу, но не количество ячеек.

25 цветов, радиус Гаусса 5 пикселей

showColors


Три цвета, радиус 17 пикселей.

nColors = 6; gaussianRadius = 3; showColors[ape, nColors, gaussianRadius] quantImg[ape, nColors, gaussianRadius]


Двадцать цветов, радиус 17 пикселей.

Мы увеличили количество цветов, но не фокус. Обратите внимание на увеличение количества ячеек.


Шесть цветов, радиус 4 пикселя

nColors = 6; gaussianRadius = 17; showColors[ape, nColors, gaussianRadius] quantImg[ape, nColors, gaussianRadius]


nColors=6;gaussianRadius=4; showColors[wave,nColors,gaussianRadius] quantImg[wave,nColors,gaussianRadius]


nColors=3;gaussianRadius=17; showColors[wave,nColors,gaussianRadius] quantImg[wave,nColors,gaussianRadius]


Звездная ночь

Всего 6 цветов и 60 ячеек. nColors = 25; gR = 5; quantImg[balls, nColors, gR] claims it uses. (Yellow doesn't appear among the 5 colors but it is used in the drawing.) I'll see if I can figure this out.

Цвета не совпадают по цвету

 

222sw


Рег
05 Nov, 2019

Тем
83

Постов
210

Баллов
645
  • 26, Oct 2024
  • #5

Python 2 с PIL

gaussianRadius[img_,nCol_,nCells_,initialRadius_:0]:= Module[{radius=initialRadius,nc=10^6,results={},r}, While[nc>nCells,(nc=numberOfCells[ape,nColors,radius]); results=AppendTo[results,{nColors,radius,nc}];radius++]; r=results[[{-2,-1}]]; Nearest[r[[All,3]],200][[1]]; Cases[r,{_,_,Nearest[r[[All,3]],nCells][[1]]}][[1,2]] ] quantImg2[img_,nColours_,nCells1_,initialRadius_:0]:={ColorQuantize[GaussianFilter[img, g=gaussianRadius[img,nColours,nCells1,initialRadius]],nColours,Dithering->False], nCells1,nn=numberOfCells[img,nColours,g],N[(nn-nCells1)/nCells1],g}

Это все еще в стадии разработки. Кроме того, приведенный ниже код представляет собой ужасную мешанину спагетти, и его не следует использовать в качестве вдохновения. :)

Как это работает quantImage2 regions, each of which consists of some number of cells without holes. Initially, the canvas is just divided into approximate squares, which are randomly assigned to the regions. Then, these regions are "deformed" in an iterative process, where a given pixel can change its region if

  1. Программа делит холст на
  2. изменение уменьшит расстояние RGB пикселя от среднего цвета области, которая его содержит, и

он не разрывает и не объединяет ячейки и не создает в них дыр. quantImage2 cells at that time. Cells can also disappear if their size is 1, and this makes room for the filaments cuts.

Последнее условие может быть реализовано локально, поэтому процесс чем-то напоминает клеточный автомат. Таким образом, нам не нужно выполнять какой-либо поиск пути или что-то в этом роде, что значительно ускоряет процесс. Однако, поскольку клетки невозможно разделить, некоторые из них превращаются в длинные «нити», граничащие с другими клетками и подавляющие их рост. Чтобы исправить это, существует процесс, называемый «разрезанием нити», который иногда разбивает нитевидную клетку на две части, если их меньше

Процесс заканчивается, когда ни у одного пикселя нет стимула переключать регионы, и после этого каждый регион просто окрашивается своим средним цветом. Обычно на выходе остаются некоторые нити, как это видно на примерах ниже, особенно в туманности.

П = 30, Н = 500

Больше фотографий позже. quantImg[img_,nColours_,gaussR_]:=ColorQuantize[GaussianFilter[img,gaussR],nColours, Dithering-> False] colours[qImg_]:=Union[Flatten[ImageData[qImg],1]] showColors[image_,nColors_,gaussR_]:= Module[{qImg,colors,ca,nCells}, qImg=quantImg[image,nColors,gaussR]; colors=colours[qImg]; ca=ConstantArray[0,Reverse@ImageDimensions[image]]; nCells[qImgg_,color_]:= Module[{r}, r=ReplacePart[ca,Position[ImageData@qImg,color]/.{a_,b_}:> ({a,b}->1)]; (*ArrayPlot[r,ColorRules->{1\[Rule]RGBColor[color],0\[Rule]White}];*) m=MorphologicalComponents[r]; {RGBColor@color,Max[Union@Flatten[m,1]]}]; s=nCells[qImg,#]&/@colors; Grid[{ {Row[{s}]}, {Row[{"cells:\t\t",Tr[s[[All,2]]]}]},{Row[{"colors:\t\t",nColors}]}, {Row[{"Gauss. Radius: ", gaussR}]}},Alignment->Left]] colors and almost all from PIL import Image, ImageFilter import random def draw(file_name, P, N, M=3): img = Image.open(file_name, 'r') pixels = img.load() size_x, size_y = img.size def dist(c1, c2): return (c1[0]-c2[0])**2+(c1[1]-c2[1])**2+(c1[2]-c2[2])**2 def mean(colours): n = len(colours) r = sum(c[0] for c in colours)//n g = sum(c[1] for c in colours)//n b = sum(c[2] for c in colours)//n return (r,g,b) def colourize(colour, palette): return min(palette, key=lambda c: dist(c, colour)) def cluster(colours, k, max_n=10000, max_i=10): colours = random.sample(colours, max_n) centroids = random.sample(colours, k) i = 0 old_centroids = None while not(i>max_i or centroids==old_centroids): old_centroids = centroids i += 1 labels = [colourize(c, centroids) for c in colours] centroids = [mean([c for c,l in zip(colours, labels) if l is cen]) for cen in centroids] return centroids all_coords = [(x,y) for x in xrange(size_x) for y in xrange(size_y)] all_colours = [pixels[x,y] for x,y in all_coords] palette = cluster(all_colours, P) print 'clustered' for x,y in all_coords: pixels[x,y] = colourize(pixels[x,y], palette) print 'colourized' median_filter = ImageFilter.MedianFilter(size=M) img = img.filter(median_filter) pixels = img.load() for x,y in all_coords: pixels[x,y] = colourize(pixels[x,y], palette) print 'median filtered' def neighbours(edge, outer, colour=None): return set((x+a,y+b) for x,y in edge for a,b in ((1,0), (-1,0), (0,1), (0,-1)) if (x+a,y+b) in outer and (colour==None or pixels[(x+a,y+b)]==colour)) def cell(centre, rest): colour = pixels[centre] edge = set([centre]) region = set() while edge: region |= edge rest = rest-edge edge = set(n for n in neighbours(edge, rest, colour)) return region, rest print 'start segmentation:' rest = set(all_coords) cells = [] while rest: centre = random.sample(rest, 1)[0] region, rest = cell(centre, rest-set(centre)) cells += [region] print '%d pixels remaining'%len(rest) cells = sorted(cells, key=len, reverse=True) print 'segmented (%d segments)'%len(cells) print 'start merging:' while len(cells)>N: small_cell = cells.pop() n = neighbours(small_cell, set(all_coords)-small_cell) for big_cell in cells: if big_cell & n: big_cell |= small_cell break print '%d segments remaining'%len(cells) print 'merged' for cell in cells: colour = colourize(mean([pixels[x,y] for x,y in cell]), palette) for x,y in cell: pixels[x,y] = colour print 'colorized again' img.save('P%d N%d '%(P,N)+file_name) print 'saved' draw('a.png', 11, 500, 1) Некоторые интересные свойства моей программы заключаются в том, что она является вероятностной, поэтому результаты могут различаться в зависимости от разных запусков, если, конечно, вы не используете одно и то же псевдослучайное начальное число. Однако случайность не является существенной, я просто хотел избежать любых случайных артефактов, которые могут возникнуть из-за особого способа прохождения Python набора координат или чего-то подобного. Программа имеет тенденцию использовать все from __future__ import division from PIL import Image import random, math, time from collections import Counter, defaultdict, namedtuple """ Configure settings here """ INFILE = "spheres.png" OUTFILE_STEM = "out" P = 30 N = 300 OUTPUT_ALL = True # Whether to output the image at each step FLOOD_FILL_TOLERANCE = 10 CLOSE_CELL_TOLERANCE = 5 SMALL_CELL_THRESHOLD = 10 FIRST_PASS_N_RATIO = 1.5 K_MEANS_TRIALS = 30 BLUR_RADIUS = 2 BLUR_RUNS = 3 """ Color conversion functions """ X = xrange # http://www.easyrgb.com/?X=MATH def rgb2xyz(rgb): r,g,b=rgb;r/=255;g/=255;b/=255;r=((r+0.055)/1.055)**2.4 if r>0.04045 else r/12.92 g=((g+0.055)/1.055)**2.4 if g>0.04045 else g/12.92;b=((b+0.055)/1.055)**2.4 if b>0.04045 else b/12.92 r*=100;g*=100;b*=100;x=r*0.4124+g*0.3576+b*0.1805;y=r*0.2126+g*0.7152+b*0.0722 z=r*0.0193+g*0.1192+b*0.9505;return(x,y,z) def xyz2lab(xyz): x,y,z=xyz;x/=95.047;y/=100;z/=108.883;x=x**(1/3)if x>0.008856 else 7.787*x+16/116 y=y**(1/3)if y>0.008856 else 7.787*y+16/116;z=z**(1/3)if z>0.008856 else 7.787*z + 16/116 L=116*y-16;a=500*(x-y);b=200*(y-z);return(L,a,b) def rgb2lab(rgb):return xyz2lab(rgb2xyz(rgb)) def lab2xyz(lab): L,a,b=lab;y=(L+16)/116;x=a/500+y;z=y-b/200;y=y**3 if y**3>0.008856 else(y-16/116)/7.787 x=x**3 if x**3>0.008856 else (x-16/116)/7.787;z=z**3 if z**3>0.008856 else(z-16/116)/7.787 x*=95.047;y*=100;z*=108.883;return(x,y,z) def xyz2rgb(xyz): x,y,z=xyz;x/=100;y/=100;z/=100;r=x*3.2406+y*-1.5372+z*-0.4986 g=x*-0.9689+y*1.8758+z*0.0415;b=x*0.0557+y*-0.2040+z*1.0570 r=1.055*(r**(1/2.4))-0.055 if r>0.0031308 else 12.92*r;g=1.055*(g**(1/2.4))-0.055 if g>0.0031308 else 12.92*g b=1.055*(b**(1/2.4))-0.055 if b>0.0031308 else 12.92*b;r*=255;g*=255;b*=255;return(r,g,b) def lab2rgb(lab):rgb=xyz2rgb(lab2xyz(lab));return tuple([int(round(x))for x in rgb]) """ Stage 1: Read in image and convert to CIELAB """ total_time = time.time() im = Image.open(INFILE) width, height = im.size if OUTPUT_ALL: im.save(OUTFILE_STEM + "0.png") print "Saved image %s0.png" % OUTFILE_STEM def make_pixlab_map(im): width, height = im.size pixlab_map = {} for i in X(width): for j in X(height): pixlab_map[(i, j)] = rgb2lab(im.getpixel((i, j))) return pixlab_map pixlab_map = make_pixlab_map(im) print "Stage 1: CIELAB conversion complete" """ Stage 2: Partitioning the image into like-colored cells using flood fill """ def d(color1, color2): return (abs(color1[0]-color2[0])**2 + abs(color1[1]-color2[1])**2 + abs(color1[2]-color2[2])**2)**.5 def neighbours(pixel): results = [] for neighbour in [(pixel[0]+1, pixel[1]), (pixel[0]-1, pixel[1]), (pixel[0], pixel[1]+1), (pixel[0], pixel[1]-1)]: if 0 <= neighbour[0] < width and 0 <= neighbour[1] < height: results.append(neighbour) return results def flood_fill(start_pixel): to_search = {start_pixel} cell = set() searched = set() start_color = pixlab_map[start_pixel] while to_search: pixel = to_search.pop() if d(start_color, pixlab_map[pixel]) < FLOOD_FILL_TOLERANCE: cell.add(pixel) unplaced_pixels.remove(pixel) for n in neighbours(pixel): if n in unplaced_pixels and n not in cell and n not in searched: to_search.add(n) else: searched.add(pixel) return cell # These two maps are inverses, pixel/s <-> number of cell containing pixel cell_sets = {} pixcell_map = {} unplaced_pixels = {(i, j) for i in X(width) for j in X(height)} while unplaced_pixels: start_pixel = unplaced_pixels.pop() unplaced_pixels.add(start_pixel) cell = flood_fill(start_pixel) cellnum = len(cell_sets) cell_sets[cellnum] = cell for pixel in cell: pixcell_map[pixel] = cellnum print "Stage 2: Flood fill partitioning complete, %d cells" % len(cell_sets) """ Stage 3: Merge cells with less than a specified threshold amount of pixels to reduce the number of cells Also good for getting rid of some noise """ def mean_color(cell, color_map): L_sum = 0 a_sum = 0 b_sum = 0 for pixel in cell: L, a, b = color_map[pixel] L_sum += L a_sum += a b_sum += b return L_sum/len(cell), a_sum/len(cell), b_sum/len(cell) def remove_small(cell_size): if len(cell_sets) <= N: return small_cells = [] for cellnum in cell_sets: if len(cell_sets[cellnum]) <= cell_size: small_cells.append(cellnum) for cellnum in small_cells: neighbour_cells = [] for cell in cell_sets[cellnum]: for n in neighbours(cell): neighbour_reg = pixcell_map[n] if neighbour_reg != cellnum: neighbour_cells.append(neighbour_reg) closest_cell = max(neighbour_cells, key=neighbour_cells.count) for cell in cell_sets[cellnum]: pixcell_map[cell] = closest_cell if len(cell_sets[closest_cell]) <= cell_size: small_cells.remove(closest_cell) cell_sets[closest_cell] |= cell_sets[cellnum] del cell_sets[cellnum] if len(cell_sets) <= N: return for cell_size in X(1, SMALL_CELL_THRESHOLD): remove_small(cell_size) if OUTPUT_ALL: frame_im = Image.new("RGB", im.size) for cellnum in cell_sets: cell_color = mean_color(cell_sets[cellnum], pixlab_map) for pixel in cell_sets[cellnum]: frame_im.putpixel(pixel, lab2rgb(cell_color)) frame_im.save(OUTFILE_STEM + "1.png") print "Saved image %s1.png" % OUTFILE_STEM print "Stage 3: Small cell merging complete, %d cells" % len(cell_sets) """ Stage 4: Close color merging """ cell_means = {} for cellnum in cell_sets: cell_means[cellnum] = mean_color(cell_sets[cellnum], pixlab_map) n_graph = defaultdict(set) for i in X(width): for j in X(height): pixel = (i, j) cell = pixcell_map[pixel] for n in neighbours(pixel): neighbour_cell = pixcell_map[n] if neighbour_cell != cell: n_graph[cell].add(neighbour_cell) n_graph[neighbour_cell].add(cell) def merge_cells(merge_from, merge_to): merge_from_cell = cell_sets[merge_from] for pixel in merge_from_cell: pixcell_map[pixel] = merge_to del cell_sets[merge_from] del cell_means[merge_from] n_graph[merge_to] |= n_graph[merge_from] n_graph[merge_to].remove(merge_to) for n in n_graph[merge_from]: n_graph[n].remove(merge_from) if n != merge_to: n_graph[n].add(merge_to) del n_graph[merge_from] cell_sets[merge_to] |= merge_from_cell cell_means[merge_to] = mean_color(cell_sets[merge_to], pixlab_map) # Go through the cells from largest to smallest. Keep replenishing the list while we can still merge. last_time = time.time() to_search = sorted(cell_sets.keys(), key=lambda x:len(cell_sets[x]), reverse=True) full_list = True while len(cell_sets) > N and to_search: if time.time() - last_time > 15: last_time = time.time() print "Close color merging... (%d cells remaining)" % len(cell_sets) while to_search: cellnum = to_search.pop() close_cells = [] for neighbour_cellnum in n_graph[cellnum]: if d(cell_means[cellnum], cell_means[neighbour_cellnum]) < CLOSE_CELL_TOLERANCE: close_cells.append(neighbour_cellnum) if close_cells: for neighbour_cellnum in close_cells: merge_cells(neighbour_cellnum, cellnum) if neighbour_cellnum in to_search: to_search.remove(neighbour_cellnum) break if full_list == True: if to_search: full_list = False else: if not to_search: to_search = sorted(cell_sets.keys(), key=lambda x:len(cell_sets[x]), reverse=True) full_list = True if OUTPUT_ALL: frame_im = Image.new("RGB", im.size) for cellnum in cell_sets: cell_color = cell_means[cellnum] for pixel in cell_sets[cellnum]: frame_im.putpixel(pixel, lab2rgb(cell_color)) frame_im.save(OUTFILE_STEM + "2.png") print "Saved image %s2.png" % OUTFILE_STEM print "Stage 4: Close color merging complete, %d cells" % len(cell_sets) """ Stage 5: N-merging - merge until <= N cells Want to merge either 1) small cells or 2) cells close in color """ # Weight score between neighbouring cells by 1) size of cell and 2) color difference def score(cell1, cell2): return d(cell_means[cell1], cell_means[cell2]) * len(cell_sets[cell1])**.5 n_scores = {} for cellnum in cell_sets: for n in n_graph[cellnum]: n_scores[(n, cellnum)] = score(n, cellnum) last_time = time.time() while len(cell_sets) > N * FIRST_PASS_N_RATIO: if time.time() - last_time > 15: last_time = time.time() print "N-merging... (%d cells remaining)" % len(cell_sets) merge_from, merge_to = min(n_scores, key=lambda x: n_scores[x]) for n in n_graph[merge_from]: del n_scores[(merge_from, n)] del n_scores[(n, merge_from)] merge_cells(merge_from, merge_to) for n in n_graph[merge_to]: n_scores[(n, merge_to)] = score(n, merge_to) n_scores[(merge_to, n)] = score(merge_to, n) if OUTPUT_ALL: frame_im = Image.new("RGB", im.size) for cellnum in cell_sets: cell_color = cell_means[cellnum] for pixel in cell_sets[cellnum]: frame_im.putpixel(pixel, lab2rgb(cell_color)) frame_im.save(OUTFILE_STEM + "3.png") print "Saved image %s3.png" % OUTFILE_STEM del n_graph, n_scores print "Stage 5: N-merging complete, %d cells" % len(cell_sets) """ Stage 6: P merging - use k-means """ def form_clusters(centroids): clusters = defaultdict(set) for cellnum in cell_sets: # Add cell to closest centroid. scores = [] for centroid in centroids: scores.append((d(centroid, cell_means[cellnum]), centroid)) scores.sort() clusters[scores[0][1]].add(cellnum) return clusters def calculate_centroid(cluster): L_sum = 0 a_sum = 0 b_sum = 0 weighting = 0 for cellnum in cluster: # Weight based on cell size color = cell_means[cellnum] cell_weight = len(cell_sets[cellnum])**.5 L_sum += color[0]*cell_weight a_sum += color[1]*cell_weight b_sum += color[2]*cell_weight weighting += cell_weight return (L_sum/weighting, a_sum/weighting, b_sum/weighting) def db_index(clusters): # Davies-Bouldin index scatter = {} for centroid, cluster in clusters.items(): scatter_score = 0 for cellnum in cluster: scatter_score += d(cell_means[cellnum], centroid) * len(cell_sets[cellnum])**.5 scatter_score /= len(cluster) scatter[centroid] = scatter_score**2 # Mean squared distance index = 0 for ci, cluster in clusters.items(): dist_scores = [] for cj in clusters: if ci != cj: dist_scores.append((scatter[ci] + scatter[cj])/d(ci, cj)) index += max(dist_scores) return index best_clusters = None best_index = None for i in X(K_MEANS_TRIALS): centroids = {cell_means[cellnum] for cellnum in random.sample(cell_sets, P)} converged = False while not converged: clusters = form_clusters(centroids) new_centroids = {calculate_centroid(cluster) for cluster in clusters.values()} if centroids == new_centroids: converged = True centroids = new_centroids index = db_index(clusters) if best_index is None or index < best_index: best_index = index best_clusters = clusters del cell_means newpix_map = {} for centroid, cluster in best_clusters.items(): for cellnum in cluster: for pixel in cell_sets[cellnum]: newpix_map[pixel] = centroid if OUTPUT_ALL: frame_im = Image.new("RGB", im.size) for pixel in newpix_map: frame_im.putpixel(pixel, lab2rgb(newpix_map[pixel])) frame_im.save(OUTFILE_STEM + "4.png") print "Saved image %s4.png" % OUTFILE_STEM print "Stage 6: P-merging complete" """ Stage 7: Approximate Gaussian smoothing See http://blog.ivank.net/fastest-gaussian-blur.html """ # Hindsight tells me I should have used a class. I hate hindsight. def vec_sum(vectors): assert(vectors and all(len(v) == len(vectors[0]) for v in vectors)) return tuple(sum(x[i] for x in vectors) for i in X(len(vectors[0]))) def linear_blur(color_list): # Can be made faster with an accumulator output = [] for i in X(len(color_list)): relevant_pixels = color_list[max(i-BLUR_RADIUS+1, 0):i+BLUR_RADIUS] pixsum = vec_sum(relevant_pixels) output.append(tuple(pixsum[i]/len(relevant_pixels) for i in X(3))) return output def horizontal_blur(): for row in X(height): colors = [blurpix_map[(i, row)] for i in X(width)] colors = linear_blur(colors) for i in X(width): blurpix_map[(i, row)] = colors[i] def vertical_blur(): for column in X(width): colors = [blurpix_map[(column, j)] for j in X(height)] colors = linear_blur(colors) for j in X(height): blurpix_map[(column, j)] = colors[j] blurpix_map = {} for i in X(width): for j in X(height): blurpix_map[(i, j)] = newpix_map[(i, j)] for i in X(BLUR_RUNS): vertical_blur() horizontal_blur() # Pixel : color of smoothed image smoothpix_map = {} for i in X(width): for j in X(height): pixel = (i, j) blur_color = blurpix_map[pixel] nearby_colors = {newpix_map[pixel]} for n in neighbours(pixel): nearby_colors.add(newpix_map[n]) smoothpix_map[pixel] = min(nearby_colors, key=lambda x: d(x, blur_color)) del newpix_map, blurpix_map if OUTPUT_ALL: frame_im = Image.new("RGB", im.size) for pixel in smoothpix_map: frame_im.putpixel(pixel, lab2rgb(smoothpix_map[pixel])) frame_im.save(OUTFILE_STEM + "5.png") print "Saved image %s5.png" % OUTFILE_STEM print "Stage 7: Smoothing complete" """ Stage 8: Flood fill pass 2 Code copy-and-paste because I'm lazy """ def flood_fill(start_pixel): to_search = {start_pixel} cell = set() searched = set() start_color = smoothpix_map[start_pixel] while to_search: pixel = to_search.pop() if start_color == smoothpix_map[pixel]: cell.add(pixel) unplaced_pixels.remove(pixel) for n in neighbours(pixel): if n in unplaced_pixels and n not in cell and n not in searched: to_search.add(n) else: searched.add(pixel) return cell cell_sets = {} pixcell_map = {} unplaced_pixels = {(i, j) for i in X(width) for j in X(height)} while unplaced_pixels: start_pixel = unplaced_pixels.pop() unplaced_pixels.add(start_pixel) cell = flood_fill(start_pixel) cellnum = len(cell_sets) cell_sets[cellnum] = cell for pixel in cell: pixcell_map[pixel] = cellnum cell_colors = {} for cellnum in cell_sets: cell_colors[cellnum] = smoothpix_map[next(iter(cell_sets[cellnum]))] print "Stage 8: Flood fill pass 2 complete, %d cells" % len(cell_sets) """ Stage 9: Small cell removal pass 2 """ def score(cell1, cell2): return d(cell_colors[cell1], cell_colors[cell2]) * len(cell_sets[cell1])**.5 def remove_small(cell_size): small_cells = [] for cellnum in cell_sets: if len(cell_sets[cellnum]) <= cell_size: small_cells.append(cellnum) for cellnum in small_cells: neighbour_cells = [] for cell in cell_sets[cellnum]: for n in neighbours(cell): neighbour_reg = pixcell_map[n] if neighbour_reg != cellnum: neighbour_cells.append(neighbour_reg) closest_cell = max(neighbour_cells, key=neighbour_cells.count) for cell in cell_sets[cellnum]: pixcell_map[cell] = closest_cell if len(cell_sets[closest_cell]) <= cell_size: small_cells.remove(closest_cell) cell_color = cell_colors[closest_cell] for pixel in cell_sets[cellnum]: smoothpix_map[pixel] = cell_color cell_sets[closest_cell] |= cell_sets[cellnum] del cell_sets[cellnum] del cell_colors[cellnum] for cell_size in X(1, SMALL_CELL_THRESHOLD): remove_small(cell_size) if OUTPUT_ALL: frame_im = Image.new("RGB", im.size) for pixel in smoothpix_map: frame_im.putpixel(pixel, lab2rgb(smoothpix_map[pixel])) frame_im.save(OUTFILE_STEM + "6.png") print "Saved image %s6.png" % OUTFILE_STEM print "Stage 9: Small cell removal pass 2 complete, %d cells" % len(cell_sets) """ Stage 10: N-merging pass 2 Necessary as stage 7 might generate *more* cells """ def merge_cells(merge_from, merge_to): merge_from_cell = cell_sets[merge_from] for pixel in merge_from_cell: pixcell_map[pixel] = merge_to del cell_sets[merge_from] del cell_colors[merge_from] n_graph[merge_to] |= n_graph[merge_from] n_graph[merge_to].remove(merge_to) for n in n_graph[merge_from]: n_graph[n].remove(merge_from) if n != merge_to: n_graph[n].add(merge_to) del n_graph[merge_from] cell_color = cell_colors[merge_to] for pixel in merge_from_cell: smoothpix_map[pixel] = cell_color cell_sets[merge_to] |= merge_from_cell n_graph = defaultdict(set) for i in X(width): for j in X(height): pixel = (i, j) cell = pixcell_map[pixel] for n in neighbours(pixel): neighbour_cell = pixcell_map[n] if neighbour_cell != cell: n_graph[cell].add(neighbour_cell) n_graph[neighbour_cell].add(cell) n_scores = {} for cellnum in cell_sets: for n in n_graph[cellnum]: n_scores[(n, cellnum)] = score(n, cellnum) last_time = time.time() while len(cell_sets) > N: if time.time() - last_time > 15: last_time = time.time() print "N-merging (pass 2)... (%d cells remaining)" % len(cell_sets) merge_from, merge_to = min(n_scores, key=lambda x: n_scores[x]) for n in n_graph[merge_from]: del n_scores[(merge_from, n)] del n_scores[(n, merge_from)] merge_cells(merge_from, merge_to) for n in n_graph[merge_to]: n_scores[(n, merge_to)] = score(n, merge_to) n_scores[(merge_to, n)] = score(merge_to, n) print "Stage 10: N-merging pass 2 complete, %d cells" % len(cell_sets) """ Stage last: Output the image! """ test_im = Image.new("RGB", im.size) for i in X(width): for j in X(height): test_im.putpixel((i, j), lab2rgb(smoothpix_map[(i, j)])) if OUTPUT_ALL: test_im.save(OUTFILE_STEM + "7.png") else: test_im.save(OUTFILE_STEM + ".png") print "Done! (Time taken: {})".format(time.time() - total_time) option, you'll get a cool series of pictures of the deformation process. I made the Mona Lisa ones into a GIF animation (shrunk 50 % to reduce the file size). If you look closely at her face and hair, you can see the filament cutting process in action.

 

Tranthanh


Рег
30 Jan, 2007

Тем
66

Постов
215

Баллов
555
Тем
403,760
Комментарии
400,028
Опыт
2,418,908

Интересно