Нарисуй изображение как карту Вороного


170

Авторы благодарности Calvin's Hobbies за продвижение моей идеи в правильном направлении.

Рассмотрим набор точек на плоскости, которые мы будем называть сайтами , и сопоставьте цвет каждому сайту. Теперь вы можете раскрасить всю плоскость, окрасив каждую точку цветом ближайшего участка. Это называется картой Вороного (или диаграммой Вороного ). В принципе, карты Вороного могут быть определены для любой метрики расстояния, но мы просто будем использовать обычное евклидово расстояние r = √(x² + y²). ( Примечание. Вам не обязательно знать, как рассчитать и отобразить один из них, чтобы участвовать в этом соревновании.)

Вот пример с 100 сайтами:

введите описание изображения здесь

Если вы посмотрите на любую ячейку, то все точки в этой ячейке будут ближе к соответствующему сайту, чем к любому другому сайту.

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

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

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

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

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

Тестовые изображения

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

Великая Волна Ежик пляж Cornell Сатурн Бурый медведь Йоши мандрил Крабовидная туманность Малыш Геобица Водопад Крик

Пляж в первом ряду нарисовал Оливия Белл и включил с ее разрешения.

Если вам нужен дополнительный вызов, попробуйте Йоши с белым фоном и нарисуйте правильную линию живота.

Вы можете найти все эти тестовые изображения в этой imgur галерее, где вы можете скачать их все в виде zip-файла. Альбом также содержит случайную диаграмму Вороного в качестве еще одного теста. Для справки, вот данные, которые его сгенерировали .

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

Если вы хотите показать большое количество результатов, вы можете создать галерею на imgur.com , чтобы размер ответов был разумным. В качестве альтернативы, поместите миниатюры в свое сообщение и сделайте их ссылками на большие изображения, как я сделал в своем справочном ответе . Вы можете получить маленькие миниатюры, добавив sимя файла в ссылку на imgur.com (например, I3XrT.png-> I3XrTs.png). Кроме того, не стесняйтесь использовать другие тестовые изображения, если вы найдете что-то хорошее.

Renderer

Вставьте свой вывод в следующий фрагмент стека, чтобы отобразить результаты. Точный формат списка не имеет значения, если в каждой ячейке заданы 5 чисел с плавающей запятой в порядке x y r g b, где xи y- это координаты сайта ячейки, а r g bтакже красный, зеленый и синий цветовые каналы в диапазоне 0 ≤ r, g, b ≤ 1.

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

Огромные благодарности Раймонду Хиллу за написание этой действительно хорошей библиотеки JS Voronoi .

Связанные проблемы


5
@frogeyedpeas Глядя на голоса, которые вы получаете. ;) Это конкурс популярности. Существует не обязательно лучший способ сделать. Идея состоит в том, что вы пытаетесь сделать это как можно лучше, и голоса будут отражать, согласны ли люди, что вы проделали хорошую работу. Правда, в этом есть определенная субъективность. Посмотрите на связанные проблемы, которые я связал, или на этот . Вы увидите, что обычно существует большое разнообразие подходов, но система голосования помогает лучшим решениям подняться на вершину и выбрать победителя.
Мартин Эндер

3
Оливия одобряет аппроксимации своего пляжа, представленные до сих пор.
Алекс А.

3
@AlexA. Девон одобряет некоторые из приближений его лица, представленные до сих пор. Он не большой поклонник ни одной из версий n = 100;)
Geobits

1
@Geobits: он поймет, когда станет старше.
Алекс А.

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

Ответы:


112

Python + scipy + scikit-image , взвешенная выборка диска Пуассона

Мое решение довольно сложное. Я делаю некоторую предварительную обработку изображения, чтобы удалить шум и получить отображение того, насколько «интересна» каждая точка (используя комбинацию локальной энтропии и обнаружения краев):

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

Затем, когда у меня есть точки отбора проб, я делю изображение на сегменты вороного и назначаю среднее значение L * a * b * значений цвета внутри каждого сегмента каждому сегменту.

У меня много эвристик, и я также должен немного посчитать, чтобы убедиться, что количество точек выборки близко N. Я получаю Nименно то, что немного перебегаю , а затем отбрасываю некоторые пункты с помощью эвристики.

С точки зрения времени выполнения этот фильтр недешев , но ни одно изображение ниже не заняло более 5 секунд.

Без дальнейших церемоний:

import math
import random
import collections
import os
import sys
import functools
import operator as op
import numpy as np
import warnings

from scipy.spatial import cKDTree as KDTree
from skimage.filters.rank import entropy
from skimage.morphology import disk, dilation
from skimage.util import img_as_ubyte
from skimage.io import imread, imsave
from skimage.color import rgb2gray, rgb2lab, lab2rgb
from skimage.filters import sobel, gaussian_filter
from skimage.restoration import denoise_bilateral
from skimage.transform import downscale_local_mean


# Returns a random real number in half-open range [0, x).
def rand(x):
    r = x
    while r == x:
        r = random.uniform(0, x)
    return r


def poisson_disc(img, n, k=30):
    h, w = img.shape[:2]

    nimg = denoise_bilateral(img, sigma_range=0.15, sigma_spatial=15)
    img_gray = rgb2gray(nimg)
    img_lab = rgb2lab(nimg)

    entropy_weight = 2**(entropy(img_as_ubyte(img_gray), disk(15)))
    entropy_weight /= np.amax(entropy_weight)
    entropy_weight = gaussian_filter(dilation(entropy_weight, disk(15)), 5)

    color = [sobel(img_lab[:, :, channel])**2 for channel in range(1, 3)]
    edge_weight = functools.reduce(op.add, color) ** (1/2) / 75
    edge_weight = dilation(edge_weight, disk(5))

    weight = (0.3*entropy_weight + 0.7*edge_weight)
    weight /= np.mean(weight)
    weight = weight

    max_dist = min(h, w) / 4
    avg_dist = math.sqrt(w * h / (n * math.pi * 0.5) ** (1.05))
    min_dist = avg_dist / 4

    dists = np.clip(avg_dist / weight, min_dist, max_dist)

    def gen_rand_point_around(point):
        radius = random.uniform(dists[point], max_dist)
        angle = rand(2 * math.pi)
        offset = np.array([radius * math.sin(angle), radius * math.cos(angle)])
        return tuple(point + offset)

    def has_neighbours(point):
        point_dist = dists[point]
        distances, idxs = tree.query(point,
                                    len(sample_points) + 1,
                                    distance_upper_bound=max_dist)

        if len(distances) == 0:
            return True

        for dist, idx in zip(distances, idxs):
            if np.isinf(dist):
                break

            if dist < point_dist and dist < dists[tuple(tree.data[idx])]:
                return True

        return False

    # Generate first point randomly.
    first_point = (rand(h), rand(w))
    to_process = [first_point]
    sample_points = [first_point]
    tree = KDTree(sample_points)

    while to_process:
        # Pop a random point.
        point = to_process.pop(random.randrange(len(to_process)))

        for _ in range(k):
            new_point = gen_rand_point_around(point)

            if (0 <= new_point[0] < h and 0 <= new_point[1] < w
                    and not has_neighbours(new_point)):
                to_process.append(new_point)
                sample_points.append(new_point)
                tree = KDTree(sample_points)
                if len(sample_points) % 1000 == 0:
                    print("Generated {} points.".format(len(sample_points)))

    print("Generated {} points.".format(len(sample_points)))

    return sample_points


def sample_colors(img, sample_points, n):
    h, w = img.shape[:2]

    print("Sampling colors...")
    tree = KDTree(np.array(sample_points))
    color_samples = collections.defaultdict(list)
    img_lab = rgb2lab(img)
    xx, yy = np.meshgrid(np.arange(h), np.arange(w))
    pixel_coords = np.c_[xx.ravel(), yy.ravel()]
    nearest = tree.query(pixel_coords)[1]

    i = 0
    for pixel_coord in pixel_coords:
        color_samples[tuple(tree.data[nearest[i]])].append(
            img_lab[tuple(pixel_coord)])
        i += 1

    print("Computing color means...")
    samples = []
    for point, colors in color_samples.items():
        avg_color = np.sum(colors, axis=0) / len(colors)
        samples.append(np.append(point, avg_color))

    if len(samples) > n:
        print("Downsampling {} to {} points...".format(len(samples), n))

    while len(samples) > n:
        tree = KDTree(np.array(samples))
        dists, neighbours = tree.query(np.array(samples), 2)
        dists = dists[:, 1]
        worst_idx = min(range(len(samples)), key=lambda i: dists[i])
        samples[neighbours[worst_idx][1]] += samples[neighbours[worst_idx][0]]
        samples[neighbours[worst_idx][1]] /= 2
        samples.pop(neighbours[worst_idx][0])

    color_samples = []
    for sample in samples:
        color = lab2rgb([[sample[2:]]])[0][0]
        color_samples.append(tuple(sample[:2][::-1]) + tuple(color))

    return color_samples


def render(img, color_samples):
    print("Rendering...")
    h, w = [2*x for x in img.shape[:2]]
    xx, yy = np.meshgrid(np.arange(h), np.arange(w))
    pixel_coords = np.c_[xx.ravel(), yy.ravel()]

    colors = np.empty([h, w, 3])
    coords = []
    for color_sample in color_samples:
        coord = tuple(x*2 for x in color_sample[:2][::-1])
        colors[coord] = color_sample[2:]
        coords.append(coord)

    tree = KDTree(coords)
    idxs = tree.query(pixel_coords)[1]
    data = colors[tuple(tree.data[idxs].astype(int).T)].reshape((w, h, 3))
    data = np.transpose(data, (1, 0, 2))

    return downscale_local_mean(data, (2, 2, 1))


if __name__ == "__main__":
    warnings.simplefilter("ignore")

    img = imread(sys.argv[1])[:, :, :3]

    print("Calibrating...")
    mult = 1.02 * 500 / len(poisson_disc(img, 500))

    for n in (100, 300, 1000, 3000):
        print("Sampling {} for size {}.".format(sys.argv[1], n))

        sample_points = poisson_disc(img, mult * n)
        samples = sample_colors(img, sample_points, n)
        base = os.path.basename(sys.argv[1])
        with open("{}-{}.txt".format(os.path.splitext(base)[0], n), "w") as f:
            for sample in samples:
                f.write(" ".join("{:.3f}".format(x) for x in sample) + "\n")

        imsave("autorenders/{}-{}.png".format(os.path.splitext(base)[0], n),
            render(img, samples))

        print("Done!")

Картинки

Соответственно Nэто 100, 300, 1000 и 3000:

азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука
азбука азбука азбука азбука


2
Мне это нравится; это выглядит как копченое стекло.
BobTheAwesome

3
Я немного повозился с этим, и вы получите лучшие результаты, особенно для изображений с низким треугольником, если вы замените denoise_bilatteral на denoise_tv_bregman. Он генерирует больше ровных пятен в своем шумоподавлении, что помогает.
Л.Клевин

@LKlevin Какой вес ты использовал?
orlp

Я использовал 1,0 в качестве веса.
Л.Клевин

65

C ++

Мой подход довольно медленный, но я очень доволен качеством результатов, которые он дает, особенно в отношении сохранения краев. Например, вот Йоши и Коробка Корнелла с всего 1000 сайтами каждый:

Есть две основные части, которые делают это галочкой. Первый, evaluate()реализованный в функции, берет набор возможных местоположений сайта, устанавливает для них оптимальные цвета и возвращает оценку PSNR визуализированной тесселяции Вороного в сравнении с целевым изображением. Цвета для каждого сайта определяются путем усреднения пикселей целевого изображения, охватываемых ячейкой вокруг сайта. Я использую алгоритм Уэлфорда, чтобы помочь рассчитать как наилучший цвет для каждой ячейки, так и полученный PSNR, используя всего один проход по изображению, используя взаимосвязь между дисперсией, MSE и PSNR. Это сводит проблему к поиску лучшего набора местоположений сайта без особого отношения к цвету.

Затем вторая часть, воплощенная в main(), пытается найти этот набор. Он начинается с выбора набора точек наугад. Затем на каждом шаге он удаляет одну точку (циклический перебор) и проверяет набор случайных точек-кандидатов для ее замены. Тот, который дает самый высокий PSNR из группы, принимается и сохраняется. По сути, это заставляет сайт переходить на новое место и, как правило, улучшает изображение побитно. Обратите внимание, что алгоритм намеренно не сохраняет исходное местоположение в качестве кандидата. Иногда это означает, что скачок снижает общее качество изображения. Это позволяет избежать застревания в локальных максимумах. Это также дает критерии остановки; программа завершается после выполнения определенного количества шагов без улучшения лучшего набора сайтов, найденных до сих пор.

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

Код

#include <cstdlib>
#include <cmath>
#include <string>
#include <vector>
#include <fstream>
#include <istream>
#include <ostream>
#include <iostream>
#include <algorithm>
#include <random>

static auto const decimation = 2;
static auto const candidates = 96;
static auto const termination = 200;

using namespace std;

struct rgb {float red, green, blue;};
struct img {int width, height; vector<rgb> pixels;};
struct site {float x, y; rgb color;};

img read(string const &name) {
    ifstream file{name, ios::in | ios::binary};
    auto result = img{0, 0, {}};
    if (file.get() != 'P' || file.get() != '6')
        return result;
    auto skip = [&](){
        while (file.peek() < '0' || '9' < file.peek())
            if (file.get() == '#')
                while (file.peek() != '\r' && file.peek() != '\n')
                    file.get();
    };
     auto maximum = 0;
     skip(); file >> result.width;
     skip(); file >> result.height;
     skip(); file >> maximum;
     file.get();
     for (auto pixel = 0; pixel < result.width * result.height; ++pixel) {
         auto red = file.get() * 1.0f / maximum;
         auto green = file.get() * 1.0f / maximum;
         auto blue = file.get() * 1.0f / maximum;
         result.pixels.emplace_back(rgb{red, green, blue});
     }
     return result;
 }

 float evaluate(img const &target, vector<site> &sites) {
     auto counts = vector<int>(sites.size());
     auto variance = vector<rgb>(sites.size());
     for (auto &site : sites)
         site.color = rgb{0.0f, 0.0f, 0.0f};
     for (auto y = 0; y < target.height; y += decimation)
         for (auto x = 0; x < target.width; x += decimation) {
             auto best = 0;
             auto closest = 1.0e30f;
             for (auto index = 0; index < sites.size(); ++index) {
                 float distance = ((x - sites[index].x) * (x - sites[index].x) +
                                   (y - sites[index].y) * (y - sites[index].y));
                 if (distance < closest) {
                     best = index;
                     closest = distance;
                 }
             }
             ++counts[best];
             auto &pixel = target.pixels[y * target.width + x];
             auto &color = sites[best].color;
             rgb delta = {pixel.red - color.red,
                          pixel.green - color.green,
                          pixel.blue - color.blue};
             color.red += delta.red / counts[best];
             color.green += delta.green / counts[best];
             color.blue += delta.blue / counts[best];
             variance[best].red += delta.red * (pixel.red - color.red);
             variance[best].green += delta.green * (pixel.green - color.green);
             variance[best].blue += delta.blue * (pixel.blue - color.blue);
         }
     auto error = 0.0f;
     auto count = 0;
     for (auto index = 0; index < sites.size(); ++index) {
         if (!counts[index]) {
             auto x = min(max(static_cast<int>(sites[index].x), 0), target.width - 1);
             auto y = min(max(static_cast<int>(sites[index].y), 0), target.height - 1);
             sites[index].color = target.pixels[y * target.width + x];
         }
         count += counts[index];
         error += variance[index].red + variance[index].green + variance[index].blue;
     }
     return 10.0f * log10f(count * 3 / error);
 }

 void write(string const &name, int const width, int const height, vector<site> const &sites) {
     ofstream file{name, ios::out};
     file << width << " " << height << endl;
     for (auto const &site : sites)
         file << site.x << " " << site.y << " "
              << site.color.red << " "<< site.color.green << " "<< site.color.blue << endl;
 }

 int main(int argc, char **argv) {
     auto rng = mt19937{random_device{}()};
     auto uniform = uniform_real_distribution<float>{0.0f, 1.0f};
     auto target = read(argv[1]);
     auto sites = vector<site>{};
     for (auto point = atoi(argv[2]); point; --point)
         sites.emplace_back(site{
             target.width * uniform(rng),
             target.height * uniform(rng)});
     auto greatest = 0.0f;
     auto remaining = termination;
     for (auto step = 0; remaining; ++step, --remaining) {
         auto best_candidate = sites;
         auto best_psnr = 0.0f;
         #pragma omp parallel for
         for (auto candidate = 0; candidate < candidates; ++candidate) {
             auto trial = sites;
             #pragma omp critical
             {
                 trial[step % sites.size()].x = target.width * (uniform(rng) * 1.2f - 0.1f);
                 trial[step % sites.size()].y = target.height * (uniform(rng) * 1.2f - 0.1f);
             }
             auto psnr = evaluate(target, trial);
             #pragma omp critical
             if (psnr > best_psnr) {
                 best_candidate = trial;
                 best_psnr = psnr;
             }
         }
         sites = best_candidate;
         if (best_psnr > greatest) {
             greatest = best_psnr;
             remaining = termination;
             write(argv[3], target.width, target.height, sites);
         }
         cout << "Step " << step << "/" << remaining
              << ", PSNR = " << best_psnr << endl;
     }
     return 0;
 }

Бег

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

Чтобы скомпилировать его, сохраните программу как voronoi.cppи запустите:

g++ -std=c++11 -fopenmp -O3 -o voronoi voronoi.cpp

Я ожидаю, что он, вероятно, будет работать в Windows с последними версиями Visual Studio, хотя я не пробовал это. Вы захотите убедиться, что вы компилируете с C ++ 11 или лучше и OpenMP включен, если вы это делаете. OpenMP не является строго необходимым, но он очень помогает сделать время выполнения более терпимым.

Чтобы запустить его, сделайте что-то вроде:

./voronoi cornell.ppm 1000 cornell-1000.txt

Более поздний файл будет обновляться с данными сайта по мере его поступления. Первая строка будет иметь ширину и высоту изображения, за которыми следуют строки значений x, y, r, g, b, подходящие для копирования и вставки в средство визуализации Javascript в описании проблемы.

Три константы в верхней части программы позволяют вам настраивать скорость и качество. decimationФактор укрупняет изображение цели при оценке набора сайтов для цвета и PSNR. Чем оно выше, тем быстрее будет работать программа. При установке значения 1 используется изображение с полным разрешением. В candidatesпостоянных управлений , сколько кандидатов для проверки на каждом шаге. Чем выше, тем больше шансов найти хорошее место для прыжка, но замедляет работу программы. И, наконец, terminationсколько шагов может пройти программа, не улучшая свой вывод перед тем, как выйти. Увеличение может дать лучшие результаты, но сделать это займет немного больше времени.

Картинки

N = 100, 300, 1000 и 3000:


1
Это должно было выиграть ИМО - намного лучше, чем мое.
orlp

1
@orlp - Спасибо! Честно говоря, вы разместили свой намного раньше, и он работает намного быстрее. Скорость имеет значение!
Boojum

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

55

IDL, адаптивное уточнение

Этот метод основан на Адаптивном уточнении сетки из астрономических симуляций, а также на поверхности подразделения . Это та задача, которой гордится IDL, о которой вы сможете судить по большому количеству встроенных функций, которые я смог использовать. : D

Я вывел некоторые промежуточные продукты для тестового изображения йоши на черном фоне, с n = 1000.

Сначала мы выполняем яркость серого на изображении (используя ct_luminance) и применяем фильтр Prewitt ( prewittсм. Википедию ) для хорошего обнаружения краев:

азбука азбука

Затем следует настоящая грубая работа: мы подразделяем изображение на 4 и измеряем дисперсию в каждом квадранте отфильтрованного изображения. Наша дисперсия взвешивается на размер подразделения (который на этом первом этапе равен), так что «острые» области с высокой дисперсией не делятся на все меньшие, меньшие и меньшие. Затем мы используем взвешенную дисперсию для более детального таргетирования подразделений и итеративно подразделяем каждый подробный раздел на 4 дополнительных, пока не достигнем целевого числа сайтов (каждое подразделение содержит ровно один сайт). Так как мы добавляем 3 сайта при каждой итерации, мы получаем n - 2 <= N <= nсайты.

Я сделал .webm процесса подразделения для этого изображения, которое не могу встроить, но оно здесь ; цвет в каждом подразделе определяется взвешенной дисперсией. (Я сделал такое же видео для йоши на белом фоне, для сравнения, таблица цветов перевернута, поэтому она идет к белому, а не к черному; это здесь .) Конечный продукт подразделения выглядит следующим образом:

азбука

Как только у нас есть список подразделений, мы проходим каждое подразделение. Конечное местоположение сайта - это позиция минимума изображения Prewitt, то есть наименее «острый» пиксель, а цвет сечения - это цвет этого пикселя; Вот оригинальное изображение с отмеченными сайтами:

азбука

Затем мы используем встроенный модуль triangulateдля вычисления триангуляции Делоне для сайтов и встроенный модуль voronoiдля определения вершин каждого многоугольника Вороного, прежде чем рисовать каждый многоугольник в буфере изображения соответствующего цвета. Наконец, мы сохраняем снимок буфера изображения.

азбука

Код:

function subdivide, image, bounds, vars
  ;subdivide a section into 4, and return the 4 subdivisions and the variance of each
  division = list()
  vars = list()
  nx = bounds[2] - bounds[0]
  ny = bounds[3] - bounds[1]
  for i=0,1 do begin
    for j=0,1 do begin
      x = i * nx/2 + bounds[0]
      y = j * ny/2 + bounds[1]
      sub = image[x:x+nx/2-(~(nx mod 2)),y:y+ny/2-(~(ny mod 2))]
      division.add, [x,y,x+nx/2-(~(nx mod 2)),y+ny/2-(~(ny mod 2))]
      vars.add, variance(sub) * n_elements(sub)
    endfor
  endfor
  return, division
end

pro voro_map, n, image, outfile
  sz = size(image, /dim)
  ;first, convert image to greyscale, and then use a Prewitt filter to pick out edges
  edges = prewitt(reform(ct_luminance(image[0,*,*], image[1,*,*], image[2,*,*])))
  ;next, iteratively subdivide the image into sections, using variance to pick
  ;the next subdivision target (variance -> detail) until we've hit N subdivisions
  subdivisions = subdivide(edges, [0,0,sz[1],sz[2]], variances)
  while subdivisions.count() lt (n - 2) do begin
    !null = max(variances.toarray(),target)
    oldsub = subdivisions.remove(target)
    newsub = subdivide(edges, oldsub, vars)
    if subdivisions.count(newsub[0]) gt 0 or subdivisions.count(newsub[1]) gt 0 or subdivisions.count(newsub[2]) gt 0 or subdivisions.count(newsub[3]) gt 0 then stop
    subdivisions += newsub
    variances.remove, target
    variances += vars
  endwhile
  ;now we find the minimum edge value of each subdivision (we want to pick representative 
  ;colors, not edge colors) and use that as the site (with associated color)
  sites = fltarr(2,n)
  colors = lonarr(n)
  foreach sub, subdivisions, i do begin
    slice = edges[sub[0]:sub[2],sub[1]:sub[3]]
    !null = min(slice,target)
    sxy = array_indices(slice, target) + sub[0:1]
    sites[*,i] = sxy
    colors[i] = cgcolor24(image[0:2,sxy[0],sxy[1]])
  endforeach
  ;finally, generate the voronoi map
  old = !d.NAME
  set_plot, 'Z'
  device, set_resolution=sz[1:2], decomposed=1, set_pixel_depth=24
  triangulate, sites[0,*], sites[1,*], tr, connectivity=C
  for i=0,n-1 do begin
    if C[i] eq C[i+1] then continue
    voronoi, sites[0,*], sites[1,*], i, C, xp, yp
    cgpolygon, xp, yp, color=colors[i], /fill, /device
  endfor
  !null = cgsnapshot(file=outfile, /nodialog)
  set_plot, old
end

pro wrapper
  cd, '~/voronoi'
  fs = file_search()
  foreach f,fs do begin
    base = strsplit(f,'.',/extract)
    if base[1] eq 'png' then im = read_png(f) else read_jpeg, f, im
    voro_map,100, im, base[0]+'100.png'
    voro_map,500, im, base[0]+'500.png'
    voro_map,1000,im, base[0]+'1000.png'
  endforeach
end

Звоните через voro_map, n, image, output_filename. Я также включил wrapperпроцедуру, которая проходила через каждое тестовое изображение и выполнялась на 100, 500 и 1000 сайтах.

Выходные данные собраны здесь , а вот несколько миниатюр:

n = 100

азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука

n = 500

азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука

n = 1000

азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука азбука


9
Мне действительно нравится тот факт, что это решение ставит больше точек в более сложных областях, что, как я думаю, является намерением, и отличает его от других на данный момент.
Александр-Бретт

да, идея детально сгруппированных точек - это то, что привело меня к адаптивному уточнению
sirpercival

3
Очень аккуратное объяснение, и изображения впечатляют! У меня есть вопрос - похоже, вы получаете много разных изображений, когда Йоши на белом фоне, где у нас есть несколько странных форм. Что может быть причиной этого?
BrainSteel

2
@BrianSteel Я думаю, что контуры выбираются как области с высокой дисперсией и фокусируются на излишне, а затем другие действительно высокодетальные области получают меньше точек из-за этого.
doppelgreener

@BrainSteel Я думаю, что Доппель прав - между черной границей и белым фоном есть сильная грань, которая требует большого количества деталей в алгоритме. я не уверен, что это то, что я могу (или, что более важно, должно ) исправить ...
sirpercival

47

Python 3 + PIL + SciPy, Fuzzy k-means

from collections import defaultdict
import itertools
import random
import time

from PIL import Image
import numpy as np
from scipy.spatial import KDTree, Delaunay

INFILE = "planet.jpg"
OUTFILE = "voronoi.txt"
N = 3000

DEBUG = True # Outputs extra images to see what's happening
FEATURE_FILE = "features.png"
SAMPLE_FILE = "samples.png"
SAMPLE_POINTS = 20000
ITERATIONS = 10
CLOSE_COLOR_THRESHOLD = 15

"""
Color conversion functions
"""

start_time = time.time()

# 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):
  return xyz2rgb(lab2xyz(lab))

"""
Step 1: Read image and convert to CIELAB
"""

im = Image.open(INFILE)
im = im.convert("RGB")
width, height = prev_size = im.size

pixlab_map = {}

for x in range(width):
    for y in range(height):
        pixlab_map[(x, y)] = rgb2lab(im.getpixel((x, y)))

print("Step 1: Image read and converted")

"""
Step 2: Get feature points
"""

def euclidean(point1, point2):
    return sum((x-y)**2 for x,y in zip(point1, point2))**.5


def neighbours(pixel):
    x, y = pixel
    results = []

    for dx, dy in itertools.product([-1, 0, 1], repeat=2):
        neighbour = (pixel[0] + dx, pixel[1] + dy)

        if (neighbour != pixel and 0 <= neighbour[0] < width
            and 0 <= neighbour[1] < height):
            results.append(neighbour)

    return results

def mse(colors, base):
    return sum(euclidean(x, base)**2 for x in colors)/len(colors)

features = []

for x in range(width):
    for y in range(height):
        pixel = (x, y)
        col = pixlab_map[pixel]
        features.append((mse([pixlab_map[n] for n in neighbours(pixel)], col),
                         random.random(),
                         pixel))

features.sort()
features_copy = [x[2] for x in features]

if DEBUG:
    test_im = Image.new("RGB", im.size)

    for i in range(len(features)):
        pixel = features[i][1]
        test_im.putpixel(pixel, (int(255*i/len(features)),)*3)

    test_im.save(FEATURE_FILE)

print("Step 2a: Edge detection-ish complete")

def random_index(list_):
    r = random.expovariate(2)

    while r > 1:
         r = random.expovariate(2)

    return int((1 - r) * len(list_))

sample_points = set()

while features and len(sample_points) < SAMPLE_POINTS:
    index = random_index(features)
    point = features[index][2]
    sample_points.add(point)
    del features[index]

print("Step 2b: {} feature samples generated".format(len(sample_points)))

if DEBUG:
    test_im = Image.new("RGB", im.size)

    for pixel in sample_points:
        test_im.putpixel(pixel, (255, 255, 255))

    test_im.save(SAMPLE_FILE)

"""
Step 3: Fuzzy k-means
"""

def euclidean(point1, point2):
    return sum((x-y)**2 for x,y in zip(point1, point2))**.5

def get_centroid(points):
    return tuple(sum(coord)/len(points) for coord in zip(*points))

def mean_cell_color(cell):
    return get_centroid([pixlab_map[pixel] for pixel in cell])

def median_cell_color(cell):
    # Pick start point out of mean and up to 10 pixels in cell
    mean_col = get_centroid([pixlab_map[pixel] for pixel in cell])
    start_choices = [pixlab_map[pixel] for pixel in cell]

    if len(start_choices) > 10:
        start_choices = random.sample(start_choices, 10)

    start_choices.append(mean_col)

    best_dist = None
    col = None

    for c in start_choices:
        dist = sum(euclidean(c, pixlab_map[pixel])
                       for pixel in cell)

        if col is None or dist < best_dist:
            col = c
            best_dist = dist

    # Approximate median by hill climbing
    last = None

    while last is None or euclidean(col, last) < 1e-6:
        last = col

        best_dist = None
        best_col = None

        for deviation in itertools.product([-1, 0, 1], repeat=3):
            new_col = tuple(x+y for x,y in zip(col, deviation))
            dist = sum(euclidean(new_col, pixlab_map[pixel])
                       for pixel in cell)

            if best_dist is None or dist < best_dist:
                best_col = new_col

        col = best_col

    return col

def random_point():
    index = random_index(features_copy)
    point = features_copy[index]

    dx = random.random() * 10 - 5
    dy = random.random() * 10 - 5

    return (point[0] + dx, point[1] + dy)

centroids = np.asarray([random_point() for _ in range(N)])
variance = {i:float("inf") for i in range(N)}
cluster_colors = {i:(0, 0, 0) for i in range(N)}

# Initial iteration
tree = KDTree(centroids)
clusters = defaultdict(set)

for point in sample_points:
    nearest = tree.query(point)[1]
    clusters[nearest].add(point)

# Cluster!
for iter_num in range(ITERATIONS):
    if DEBUG:
        test_im = Image.new("RGB", im.size)

        for n, pixels in clusters.items():
            color = 0xFFFFFF * (n/N)
            color = (int(color//256//256%256), int(color//256%256), int(color%256))

            for p in pixels:
                test_im.putpixel(p, color)

        test_im.save(SAMPLE_FILE)

    for cluster_num in clusters:
        if clusters[cluster_num]:
            cols = [pixlab_map[x] for x in clusters[cluster_num]]

            cluster_colors[cluster_num] = mean_cell_color(clusters[cluster_num])
            variance[cluster_num] = mse(cols, cluster_colors[cluster_num])

        else:
            cluster_colors[cluster_num] = (0, 0, 0)
            variance[cluster_num] = float("inf")

    print("Clustering (iteration {})".format(iter_num))

    # Remove useless/high variance
    if iter_num < ITERATIONS - 1:
        delaunay = Delaunay(np.asarray(centroids))
        neighbours = defaultdict(set)

        for simplex in delaunay.simplices:
            n1, n2, n3 = simplex

            neighbours[n1] |= {n2, n3}
            neighbours[n2] |= {n1, n3}
            neighbours[n3] |= {n1, n2}

        for num, centroid in enumerate(centroids):
            col = cluster_colors[num]

            like_neighbours = True

            nns = set() # neighbours + neighbours of neighbours

            for n in neighbours[num]:
                nns |= {n} | neighbours[n] - {num}

            nn_far = sum(euclidean(col, cluster_colors[nn]) > CLOSE_COLOR_THRESHOLD
                         for nn in nns)

            if nns and nn_far / len(nns) < 1/5:
                sample_points -= clusters[num]

                for _ in clusters[num]:
                    if features and len(sample_points) < SAMPLE_POINTS:
                        index = random_index(features)
                        point = features[index][3]
                        sample_points.add(point)
                        del features[index]

                clusters[num] = set()

    new_centroids = []

    for i in range(N):
        if clusters[i]:
            new_centroids.append(get_centroid(clusters[i]))
        else:
            new_centroids.append(random_point())

    centroids = np.asarray(new_centroids)
    tree = KDTree(centroids)

    clusters = defaultdict(set)

    for point in sample_points:
        nearest = tree.query(point, k=6)[1]
        col = pixlab_map[point]

        for n in nearest:
            if n < N and euclidean(col, cluster_colors[n])**2 <= variance[n]:
                clusters[n].add(point)
                break

        else:
            clusters[nearest[0]].add(point)

print("Step 3: Fuzzy k-means complete")

"""
Step 4: Output
"""

for i in range(N):
    if clusters[i]:
        centroids[i] = get_centroid(clusters[i])

centroids = np.asarray(centroids)
tree = KDTree(centroids)
color_clusters = defaultdict(set)

# Throw back on some sample points to get the colors right
all_points = [(x, y) for x in range(width) for y in range(height)]

for pixel in random.sample(all_points, int(min(width*height, 5 * SAMPLE_POINTS))):
    nearest = tree.query(pixel)[1]
    color_clusters[nearest].add(pixel)

with open(OUTFILE, "w") as outfile:
    for i in range(N):
        if clusters[i]:
            centroid = tuple(centroids[i])          
            col = tuple(x/255 for x in lab2rgb(median_cell_color(color_clusters[i] or clusters[i])))
            print(" ".join(map(str, centroid + col)), file=outfile)

print("Done! Time taken:", time.time() - start_time)

Алгоритм

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

Сначала мы конвертируем каждый пиксель в цветовое пространство Lab для лучшей цветопередачи.

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

введите описание изображения здесь

(Существует четкий узор в виде сетки на приведенном выше выводе. Согласно @randomra, это, вероятно, связано с кодированием JPG с потерями или сжатием изображений imgur.)

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

введите описание изображения здесь

Для кластеризации мы сначала выбираем Nцентроиды, случайно выбранные с использованием того же экспоненциального распределения, что и выше. Выполняется начальная итерация, и для каждого из полученных кластеров мы назначаем средний цвет и порог цветовой дисперсии. Затем для ряда итераций мы:

  • Создайте триангуляцию Делоне по центроидам, чтобы мы могли легко запрашивать соседей по центроидам.
  • Используйте триангуляцию, чтобы удалить центроиды, близкие по цвету к большинству (> 4/5) их соседей и соседей, вместе взятых. Все связанные точки выборки также удаляются, и добавляются новые замещающие центроиды и точки выборки. Этот шаг пытается заставить алгоритм разместить больше кластеров там, где нужны детали.
  • Создайте kd-дерево для новых центроидов, чтобы мы могли легко запросить ближайшие центроиды к любой точке выборки.
  • Используйте дерево, чтобы назначить каждую точку выборки одному из 6 ближайших центроидов (6 выбраны опытным путем). Центроид принимает точку выборки только в том случае, если цвет точки находится в пределах порога цветовой дисперсии центроида. Мы пытаемся назначить каждую точку выборки первому принимающему центроиду, но если это невозможно, мы просто назначаем ее ближайшему центроиду. «Нечеткость» алгоритма проистекает из этого шага, поскольку кластеры могут перекрываться.
  • Пересчитать центроиды.

введите описание изображения здесь

(Нажмите для увеличения)

Наконец, мы отбираем большое количество точек, на этот раз используя равномерное распределение. Используя другое kd-дерево, мы присваиваем каждую точку ближайшему центроиду, образуя кластеры. Затем мы аппроксимируем срединный цвет каждого кластера, используя алгоритм восхождения на холм, давая наши окончательные цвета ячеек (идея для этого шага благодаря @PhiNotPi и @ MartinBüttner).

введите описание изображения здесь

Примечания

В дополнение к выводу текстового файла для сниппета ( OUTFILE), если DEBUGон установлен, Trueпрограмма также выведет и перезапишет изображения выше. Алгоритм занимает пару минут для каждого изображения, так что это хороший способ проверки прогресса, который не добавляет слишком много времени.

Пример выходов

N = 32:

введите описание изображения здесь

N = 100:

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь

N = 1000:

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь

N = 3000:

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь


1
Мне очень нравится, как хорошо получился твой по-белому Йоши.
Макс

26

Mathematica, Случайные ячейки

Это базовое решение, поэтому вы получите представление о минимуме, о котором я вас спрашиваю. Учитывая имя файла (локально или в виде URL) в fileи N в n, следующий код просто выбирает N случайных пикселей и использует цвета, найденные в этих пикселях. Это действительно наивно и работает не очень хорошо, но я хочу, чтобы вы, ребята, победили это в конце концов. :)

data = ImageData@Import@file;
dims = Dimensions[data][[1 ;; 2]]
{Reverse@#, data[[##]][[1 ;; 3]] & @@ Floor[1 + #]} &[dims #] & /@ 
 RandomReal[1, {n, 2}]

Вот все тестовые изображения для N = 100 (все изображения ссылаются на увеличенную версию):

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь

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

Для N = 500 ситуация немного улучшается, но все еще остаются очень странные артефакты, изображения выглядят размытыми, и много ячеек теряется в областях без деталей:

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь

Твой ход!


Я не очень хороший кодер, но, боже мой, эти картинки красиво выглядят. Отличная идея!
Фараз Масрур

Есть причина Dimensions@ImageDataи нет ImageDimensions? Вы можете избежать медленного в ImageDataцелом, используя PixelValue.
2012 год,

@ 2012rcampion Нет причин, я просто не знал, существует ли какая-либо функция. Я мог бы исправить это позже, а также изменить примеры изображений на рекомендуемые N-значения.
Мартин Эндер

23

Mathematica

Мы все знаем, что Мартин любит Mathematica, поэтому позвольте мне попробовать его с Mathematica.

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

На следующих изображениях показан пример процесса в действии. Веселье несколько испорчено плохим сглаживанием Mathematicas, но мы получаем векторную графику, которая должна чего-то стоить.

Этот алгоритм без случайной выборки можно найти в VoronoiMeshдокументации здесь .

введите описание изображения здесь введите описание изображения здесь

Тестовые изображения (100,300,1000,3000)

Код

VoronoiImage[img_, nSeeds_, iterations_] := Module[{
    i = img,
    edges = EdgeDetect@img,
    voronoiRegion = Transpose[{{0, 0}, ImageDimensions[img]}],
    seeds, voronoiInitial, voronoiRelaxed
    },
   seeds = RandomChoice[ImageValuePositions[edges, White], nSeeds];
   voronoiInitial = VoronoiMesh[seeds, voronoiRegion];
   voronoiRelaxed = 
    Nest[VoronoiMesh[Mean @@@ MeshPrimitives[#, 2], voronoiRegion] &, 
     voronoiInitial, iterations];
   Graphics[Table[{RGBColor[ImageValue[img, Mean @@ mp]], mp}, 
     {mp,MeshPrimitives[voronoiRelaxed, 2]}]]
   ];

Отличная работа для первого поста! :) Возможно, вы захотите попробовать тестовое изображение Вороного с 32 ячейками (это количество ячеек в самом изображении).
Мартин Эндер

Спасибо! Я предполагаю, что мой алгоритм будет работать ужасно на этом примере. Семена будут инициализированы по краям клетки, и рекурсия не сделает это намного лучше;)
лапа

Несмотря на более медленную скорость сходимости к исходному изображению, я считаю, что ваш алгоритм дает очень искусный результат! (вроде улучшенной версии работ Жоржа Сёра). Отличная работа!
neizod

Вы также можете получить гладко выглядящие интерполированные цвета многоугольника, изменив свои последние строки наGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
Гистограммы

13

Python + SciPy + ведущий

Алгоритм, который я использовал, следующий:

  1. Изменение размера изображения до небольшого размера (~ 150 пикселей)
  2. Создайте нерезкое изображение максимальных значений канала (это помогает не слишком сильно выделять белые области).
  3. Возьмите абсолютное значение.
  4. Выберите случайные точки с вероятностью, пропорциональной этому изображению. Это выбирает точки по обе стороны от разрывов.
  5. Уточните выбранные точки, чтобы снизить функцию стоимости. Функция является максимумом суммы квадратов отклонений в каналах (опять же помогает смещению в сплошные цвета, а не только в сплошной белый). Я неправильно использовал Марковскую цепь Монте-Карло с модулем emcee (очень рекомендуется) в качестве оптимизатора. Процедура выручает, когда никакого нового улучшения не найдено после N итераций цепочки.

Алгоритм, кажется, работает очень хорошо. К сожалению, он может работать только на маленьких изображениях. У меня не было времени взять очки Вороного и применить их к большим изображениям. Они могут быть уточнены на этом этапе. Я мог бы также запустить MCMC дольше, чтобы получить лучшие минимумы. Слабым местом алгоритма является то, что он довольно дорогой. Я не успел подняться выше 1000 пунктов, и пара изображений из 1000 точек на самом деле все еще уточняется.

(щелкните правой кнопкой мыши и просмотрите изображение, чтобы получить увеличенную версию)

Миниатюры на 100, 300 и 1000 баллов

Ссылки на более крупные версии: http://imgur.com/a/2IXDT#9 (100 баллов), http://imgur.com/a/bBQ7q (300 баллов) и http://imgur.com/a/rr8wJ. (1000 баллов)

#!/usr/bin/env python

import glob
import os

import scipy.misc
import scipy.spatial
import scipy.signal
import numpy as N
import numpy.random as NR
import emcee

def compute_image(pars, rimg, gimg, bimg):
    npts = len(pars) // 2
    x = pars[:npts]
    y = pars[npts:npts*2]
    yw, xw = rimg.shape

    # exit if points are too far away from image, to stop MCMC
    # wandering off
    if(N.any(x > 1.2*xw) or N.any(x < -0.2*xw) or
       N.any(y > 1.2*yw) or N.any(y < -0.2*yw)):
        return None

    # compute tesselation
    xy = N.column_stack( (x, y) )
    tree = scipy.spatial.cKDTree(xy)

    ypts, xpts = N.indices((yw, xw))
    queryxy = N.column_stack((N.ravel(xpts), N.ravel(ypts)))

    dist, idx = tree.query(queryxy)

    idx = idx.reshape(yw, xw)
    ridx = N.ravel(idx)

    # tesselate image
    div = 1./N.clip(N.bincount(ridx), 1, 1e99)
    rav = N.bincount(ridx, weights=N.ravel(rimg)) * div
    gav = N.bincount(ridx, weights=N.ravel(gimg)) * div
    bav = N.bincount(ridx, weights=N.ravel(bimg)) * div

    rout = rav[idx]
    gout = gav[idx]
    bout = bav[idx]
    return rout, gout, bout

def compute_fit(pars, img_r, img_g, img_b):
    """Return fit statistic for parameters."""
    # get model
    retn = compute_image(pars, img_r, img_g, img_b)
    if retn is None:
        return -1e99
    model_r, model_g, model_b = retn

    # maximum squared deviation from one of the chanels
    fit = max( ((img_r-model_r)**2).sum(),
               ((img_g-model_g)**2).sum(),
               ((img_b-model_b)**2).sum() )

    # return fake log probability
    return -fit

def convgauss(img, sigma):
    """Convolve image with a Gaussian."""
    size = 3*sigma
    kern = N.fromfunction(
        lambda y, x: N.exp( -((x-size/2)**2+(y-size/2)**2)/2./sigma ),
        (size, size))
    kern /= kern.sum()
    out = scipy.signal.convolve2d(img.astype(N.float64), kern, mode='same')
    return out

def process_image(infilename, outroot, npts):
    img = scipy.misc.imread(infilename)
    img_r = img[:,:,0]
    img_g = img[:,:,1]
    img_b = img[:,:,2]

    # scale down size
    maxdim = max(img_r.shape)
    scale = int(maxdim / 150)
    img_r = img_r[::scale, ::scale]
    img_g = img_g[::scale, ::scale]
    img_b = img_b[::scale, ::scale]

    # make unsharp-masked image of input
    img_tot = N.max((img_r, img_g, img_b), axis=0)
    img1 = convgauss(img_tot, 2)
    img2 = convgauss(img_tot, 32)
    diff = N.abs(img1 - img2)
    diff = diff/diff.max()
    diffi = (diff*255).astype(N.int)
    scipy.misc.imsave(outroot + '_unsharp.png', diffi)

    # create random points with a probability distribution given by
    # the unsharp-masked image
    yw, xw = img_r.shape
    xpars = []
    ypars = []
    while len(xpars) < npts:
        ypar = NR.randint(int(yw*0.02),int(yw*0.98))
        xpar = NR.randint(int(xw*0.02),int(xw*0.98))
        if diff[ypar, xpar] > NR.rand():
            xpars.append(xpar)
            ypars.append(ypar)

    # initial parameters to model
    allpar = N.concatenate( (xpars, ypars) )

    # set up MCMC sampler with parameters close to each other
    nwalkers = npts*5  # needs to be at least 2*number of parameters+2
    pos0 = []
    for i in xrange(nwalkers):
        pos0.append(NR.normal(0,1,allpar.shape)+allpar)

    sampler = emcee.EnsembleSampler(
        nwalkers, len(allpar), compute_fit,
        args=[img_r, img_g, img_b],
        threads=4)

    # sample until we don't find a better fit
    lastmax = -N.inf
    ct = 0
    ct_nobetter = 0
    for result in sampler.sample(pos0, iterations=10000, storechain=False):
        print ct
        pos, lnprob = result[:2]
        maxidx = N.argmax(lnprob)

        if lnprob[maxidx] > lastmax:
            # write image
            lastmax = lnprob[maxidx]
            mimg = compute_image(pos[maxidx], img_r, img_g, img_b)
            out = N.dstack(mimg).astype(N.int32)
            out = N.clip(out, 0, 255)
            scipy.misc.imsave(outroot + '_binned.png', out)

            # save parameters
            N.savetxt(outroot + '_param.dat', scale*pos[maxidx])

            ct_nobetter = 0
            print(lastmax)

        ct += 1
        ct_nobetter += 1
        if ct_nobetter == 60:
            break

def main():
    for npts in 100, 300, 1000:
        for infile in sorted(glob.glob(os.path.join('images', '*'))):
            print infile
            outroot = '%s/%s_%i' % (
                'outdir',
                os.path.splitext(os.path.basename(infile))[0], npts)

            # race condition!
            lock = outroot + '.lock'
            if os.path.exists(lock):
                continue
            with open(lock, 'w') as f:
                pass

            process_image(infile, outroot, npts)

if __name__ == '__main__':
    main()

Изображения без маскировки выглядят следующим образом. Случайные точки выбираются из изображения, если случайное число меньше, чем значение изображения (нормированное на 1):

Изображение Сатурна без маскировки

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

Редактировать: если вы увеличите число проходов до 100 * npts, измените функцию стоимости на несколько квадратов отклонений во всех каналах и ждите в течение длительного времени (увеличивая количество итераций, чтобы выйти из цикла до 200), можно сделать несколько хороших снимков всего за 100 баллов:

Изображение 11, 100 баллов Изображение 2, 100 баллов Изображение 4, 100 баллов Изображение 10, 100 баллов


3

Использование энергии изображения в качестве точечно-весовой карты

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

  1. Для каждого изображения создайте карту резкости. Карта резкости определяется нормированной энергией изображения (или квадратом высокочастотного сигнала изображения). Пример выглядит так:

Карта резкости

  1. Создайте количество точек на изображении, взяв 70 процентов от точек на карте резкости и 30 процентов от всех остальных точек. Это означает, что точки выбираются более плотно из высокодетализированных частей изображения.
  2. Цвет!

Результаты

N = 100, 500, 1000, 3000

Изображение 1, N = 100 Изображение 1, N = 500 Изображение 1, N = 1000 Изображение 1, N = 3000

Изображение 2, N = 100 Изображение 2, N = 500 Изображение 2, N = 1000 Изображение 2, N = 3000

Изображение 3, N = 100 Изображение 3, N = 500 Изображение 3, N = 1000 Изображение 3, N = 3000

Изображение 4, N = 100 Изображение 4, N = 500 Изображение 4, N = 1000 Изображение 4, N = 3000

Изображение 5, N = 100 Изображение 5, N = 500 Изображение 5, N = 1000 Изображение 5, N = 3000

Изображение 6, N = 100 Изображение 6, N = 500 Изображение 6, N = 1000 Изображение 6, N = 3000

Изображение 7, N = 100 Изображение 7, N = 500 Изображение 7, N = 1000 Изображение 7, N = 3000

Изображение 8, N = 100 Изображение 8, N = 500 Изображение 8, N = 1000 Изображение 8, N = 3000

Изображение 9, N = 100 Изображение 9, N = 500 Изображение 9, N = 1000 Изображение 9, N = 3000

Изображение 10, N = 100 Изображение 10, N = 500 Изображение 10, N = 1000 Изображение 10, N = 3000

Изображение 11, N = 100 Изображение 11, N = 500 Изображение 11, N = 1000 Изображение 11, N = 3000

Изображение 12, N = 100 Изображение 12, N = 500 Изображение 12, N = 1000 Изображение 12, N = 3000

Изображение 13, N = 100 Изображение 13, N = 500 Изображение 13, N = 1000 Изображение 13, N = 3000

Изображение 14, N = 100 Изображение 14, N = 500 Изображение 14, N = 1000 Изображение 14, N = 3000


14
Не могли бы вы а) включить исходный код, использованный для его создания, и б) связать каждую миниатюру с полноразмерным изображением?
Мартин Эндер
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.