Намалюйте зображення як карту Вороного


170

Подяка Хобі Кальвіна за те, що я підштовхнув свою правильну ідею в правильному напрямку.

Розглянемо набір точок в площині, які ми будемо називати сайтами , і пов’язати колір з кожним сайтом. Тепер ви можете пофарбувати всю площину, пофарбувавши кожну точку кольором найближчого майданчика. Це називається карта Вороного (або діаграма Вороного ). В принципі, карти Вороного можна визначити для будь-якої метрики відстані, але ми просто використаємо звичайну евклідову відстань r = √(x² + y²). ( Примітка. Вам не обов'язково знати, як обчислити та зробити один із них, щоб змагатися в цьому виклику.)

Ось приклад зі 100 сайтами:

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

Якщо ви подивитеся на будь-яку клітинку, то всі точки в цій комірці розташовані ближче до відповідного сайту, ніж до будь-якого іншого сайту.

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

Ви можете використовувати фрагмент стека в нижній частині цього виклику, щоб відобразити карту Вороного зі свого результату, або ви можете зробити їх самостійно, якщо хочете.

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

Це конкурс на популярність, тому відповідь з найбільшою кількістю чистих голосів виграє. Виборцям рекомендується судити відповіді

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

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

Ось кілька зображень для перевірки алгоритму (деякі наші звичайні підозрювані, деякі нові). Клацніть зображення для більшої версії.

Велика хвиля Їжак Пляжний Корнелл Сатурн Бурий ведмідь Йоші Mandrill Крабова туманність Малюк Геобіць Водоспад Кричати

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

Якщо ви хочете отримати додатковий виклик, спробуйте Йоші з білим тлом і підберіть його пряму лінію живота.

Ви можете знайти всі ці тестові зображення в цій галереї зображень, де їх можна завантажити у вигляді поштового файлу. Альбом також містить випадкову діаграму Вороного в якості іншого тесту. Для довідки, ось дані, які їх генерували .

Будь ласка, додайте приклади діаграм для різних зображень та N , наприклад 100, 300, 1000, 3000 (а також пастбіни до деяких відповідних специфікацій комірок). Ви можете використовувати або опускати чорні краї між клітинками, як вважаєте за потрібне (на деяких зображеннях це може виглядати краще, ніж на інших). Хоча не включайте сайти (за винятком окремого прикладу, можливо, якщо ви хочете пояснити, як працює розміщення вашого сайту, звичайно).

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

Рендерер

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

У фрагменті передбачені параметри для визначення ширини рядків ребер комірок, а також, чи слід показувати сайти комірок (останні здебільшого для налагодження). Але зауважте, що результат відображається лише тоді, коли специфікації комірок змінюються, тому, якщо ви модифікуєте деякі інші параметри, додайте пробіл у клітинки чи щось таке.

Великі кредити Реймонд Хілл за написання цієї справді прекрасної бібліотеки Дж. С. Вороного .

Супутні виклики


5
@frogeyedpeas Переглядаючи голоси, які ви отримуєте. ;) Це конкурс на популярність. Існує не обов'язково кращий спосіб зробити. Ідея полягає в тому, що ви намагаєтеся зробити це так добре, як можете, і голоси відображають, чи згодні люди, що ви добре зробили роботу. В цьому, правда, є певна кількість суб'єктивності. Погляньте на пов'язані з цим проблеми, які я пов'язав, або на цей . Ви побачите, що зазвичай існує найрізноманітніший підхід, але система голосування допомагає кращим рішенням піднятися до вершини та визначити переможця.
Мартін Ендер

3
Олівія схвалює представлені до цього часу наближення її пляжу.
Олексій А.

3
@AlexA. Девон схвалює деякі наближення його обличчя, подані до цього часу. Він не великий фанат жодної з n = 100 версій;)
Геобіт

1
@Geobits: Він зрозуміє, коли постарше.
Олексій А.

1
Ось сторінка про техніку стиплінгу на основі центральної вороної . Хороше джерело натхнення (пов'язана магістерська робота добре обговорила можливі вдосконалення алгоритму).
робота

Відповіді:


112

Python + scipy + scikit-образ , зважена дискретизація Пуассона

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

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

Потім, як тільки у мене є точки вибірки, я ділю зображення на сегменти voronoi і присвоюю середнє значення 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, використовуючи лише один прохід по зображенню, використовуючи зв'язок між дисперсією, MSE та PSNR. Це зменшує проблему до пошуку найкращого набору сайтів, не враховуючи колір.

Друга частина тоді, втілена в main(), намагається знайти цей набір. Починається з випадкового вибору набору точок. Потім на кожному кроці він видаляє одну точку (збирається кругообіг) і тестує набір випадкових кандидатів, щоб замінити її. Приймається та зберігається той, що дає найвищий рівень ПСНР згустку. Це ефективно призводить до того, що сайт переходить на нове місце і, як правило, покращує зображення. Зауважте, що алгоритм навмисно не зберігає початкове місце розташування як кандидат. Іноді це означає, що стрибок знижує загальну якість зображення. Якщо це дозволити, це допоможе не зациклюватися на локальних максимумах. Це також дає критерії зупинки; програма припиняється після проходження певної кількості кроків без вдосконалення на найкращому наборі сайтів, знайдених досі.

Зауважте, що ця реалізація є досить базовою і може зайняти години центрального процесорного часу, тим більше, що кількість сайтів зростає. Він перераховує повну карту Вороного для кожного кандидата, а груба сила перевіряє відстань до всіх сайтів для кожного пікселя. Оскільки кожна операція передбачає вилучення однієї точки одночасно та додавання іншої, фактичні зміни зображення на кожному кроці будуть досить локальними. Існують алгоритми для ефективного поступового оновлення діаграми Вороного, і я вважаю, що вони дали би цьому алгоритму величезну швидкість. Однак для цього змагання я вирішив зберігати речі просто та жорстоко.

Код

#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 - Дякую! Якщо бути справедливим, ви розмістили своє набагато раніше, і воно працює набагато швидше. Швидкість рахується!
Бужум

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
Мені дуже подобається те, що це рішення ставить більше балів у більш складних областях, що, на мій погляд, є наміром, і відрізняє його від інших на даний момент.
Олександр-Бретт

так, ідея деталізованих точок - це те, що призвело мене до адаптивного вдосконалення
серперцивал

3
Дуже акуратне пояснення, а зображення вражають! У мене питання - схоже, ви отримуєте набагато різні зображення, коли Йоші знаходиться на білому тлі, де у нас є кілька дивних форм. Що може бути причиною цього?
BrainSteel

2
@BrianSteel Я думаю, що обриси вибираються як області з великою дисперсією і зосереджуються на необгрунтованому стані, а тоді в інших справді детальних областях менше балів відведено через це.
doppelgreener

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

47

Python 3 + PIL + SciPy, нечіткі k-засоби

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-означає кластеризацію природним чином розділяє зображення на клітини Вороного, оскільки точки прив’язані до найближчого центроїда. Однак нам потрібно якось додати кольори як обмеження.

Спочатку ми перетворюємо кожен піксель у кольоровий простір лабораторії для кращого маніпулювання кольором.

Тоді ми робимо своєрідне "виявлення краю бідного чоловіка". Для кожного пікселя ми розглядаємо його ортогональні та діагональні сусіди та обчислюємо різницю кольорів середнього квадрата. Потім ми сортуємо всі пікселі за цією різницею, пікселі, найбільш схожі на своїх сусідів внизу списку, і пікселі, найбільш подібні до своїх сусідів іззаду (тобто, швидше за все, це крайова точка). Ось приклад для планети: де яскравіший піксель, тим він відрізняється від своїх сусідів:

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

(На виведеному виведенні вище є чітка сітчаста картина. Згідно @randomra, це, ймовірно, пов’язано з втратою кодування JPG або незрозумілим стисненням зображень.)

Далі ми використовуємо цей піксельний порядок для вибірки великої кількості точок, що підлягають кластеризації. Ми використовуємо експоненціальний розподіл, надаючи пріоритет кращим і цікавішим точкам.

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

Для кластеризації ми спочатку вибираємо Nцентроїди, вибрані випадковим чином, використовуючи той же експоненціальний розподіл, як вище. Виконується початкова ітерація, і для кожного з отриманих кластерів ми призначаємо середній колір та поріг відхилення кольорів. Тоді для ряду ітерацій ми:

  • Побудуйте тріангуляцію центроїдів Делоне, щоб ми могли легко запитувати сусідів до центроїдів.
  • Використовуйте тріангуляцію для видалення центроїдів, близьких за кольором до більшості (> 4/5) своїх сусідів та сусідів сусідів разом. Будь-які пов'язані точки вибірки також видаляються, додаються нові центроїди та точки вибірки. Цей крок намагається змусити алгоритм розмістити більше кластерів, де потрібна детальна інформація.
  • Побудуйте kd-дерево для нових центроїдів, щоб ми могли легко здійснити запит найближчих центроїдів до будь-якої точки вибірки.
  • Використовуйте дерево, щоб призначити кожну точку вибірки одному з 6 найближчих центроїдів (6 вибрано емпірично). Центроїд прийме точку зразка, лише якщо колір точки знаходиться в межах порогу колірної дисперсії центру. Ми намагаємося присвоїти кожній вибірковій точці перший приймаючий центроїд, але якщо це неможливо, ми просто присвоюємо йому найближчий центроїд. "Нечіткість" алгоритму випливає з цього кроку, оскільки кластери можуть перекриватися.
  • Перерахуйте центроїди.

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

(Клацніть для повного розміру)

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

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

Примітки

На додаток до виводу текстового файлу для фрагмента ( OUTFILE), якщо DEBUGвін встановлений Trueу програмі, він також виводить і замінює зображення, наведені вище. Алгоритм займає пару хвилин на кожне зображення, тому це хороший спосіб перевірити хід, який не додає дуже багато часу часу.

Зразки виходів

N = 32:

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

N = 100:

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

N = 1000:

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

N = 3000:

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


1
Мені дуже подобається, як добре вийшли твої білі Йоші.
Макс

26

Математика, випадкові клітини

Це базове рішення, тому ви отримуєте уявлення про мінімум, про який я прошу у вас. З огляду на ім’я файлу (локально або як 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.

Мій алгоритм використовує випадкові точки від країв зображення для створення початкової діаграми вороной. Потім діаграму готують за допомогою ітеративного регулювання сітки простим середнім фільтром. Це дає зображення з високою щільністю клітин поблизу областей з високим контрастом і візуально приємні клітини без шалених кутів.

На наступних зображеннях показаний приклад дії в дії. Весело дещо зіпсовано Mathematicas погано Antialiasing, але ми отримуємо векторну графіку, яка повинна щось вартувати.

Цей алгоритм без випадкової вибірки можна знайти в 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 + emcee

Я використовував такий алгоритм:

  1. Змініть розмір зображень до невеликого розміру (~ 150 пікселів)
  2. Створіть чітке маскування зображення максимальних значень каналу (це допомагає не дуже сильно підібрати білі області).
  3. Візьміть абсолютне значення.
  4. Виберіть випадкові точки з ймовірністю, пропорційною цьому зображенню. Це вибирає точки з будь-якої сторони розривів.
  5. Уточнити вибрані точки, щоб знизити функцію витрат. Функція - це максимальна сума квадратичних відхилень у каналах (знову допомагає зміщення твердих кольорів і не тільки суцільного білого). Я неправильно використовував ланцюжок Markov Monte Carlo з модулем 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 * нпт, змініть функцію витрат на деякі квадратики відхилень у всіх каналах і довго чекайте (збільшуючи кількість ітерацій, щоб вийти з циклу на 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
Ви заперечуєте проти: а) включення вихідного коду, який використовується для його створення, і b) прив'язування кожної ескізу до повнорозмірного зображення?
Мартін Ендер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.