Gambarlah sebagai Peta Voronoi


170

Penghargaan untuk Hobi Calvin karena menyenggol gagasan tantanganku ke arah yang benar.

Pertimbangkan satu set poin di pesawat, yang akan kita sebut situs , dan kaitkan warna dengan masing-masing situs. Sekarang Anda dapat mengecat seluruh bidang dengan mewarnai setiap titik dengan warna situs terdekat. Ini disebut peta Voronoi (atau diagram Voronoi ). Pada prinsipnya, peta Voronoi dapat didefinisikan untuk metrik jarak apa pun, tetapi kami hanya akan menggunakan jarak Euclidean yang biasa r = √(x² + y²). ( Catatan: Anda tidak perlu harus tahu cara menghitung dan membuat salah satunya untuk bersaing dalam tantangan ini.)

Berikut ini adalah contoh dengan 100 situs:

masukkan deskripsi gambar di sini

Jika Anda melihat sel mana pun, maka semua titik di dalam sel itu lebih dekat ke situs yang sesuai daripada ke situs lain.

Tugas Anda adalah memperkirakan gambar yang diberikan dengan peta Voronoi. Anda diberi gambar dalam format raster grafis nyaman, serta integer N . Anda kemudian harus menghasilkan hingga N situs, dan warna untuk setiap situs, sehingga peta Voronoi berdasarkan situs-situs ini menyerupai gambar input sedekat mungkin.

Anda dapat menggunakan Stack Snippet di bagian bawah tantangan ini untuk membuat peta Voronoi dari output Anda, atau Anda dapat merendernya sendiri jika Anda mau.

Anda dapat menggunakan fungsi bawaan atau pihak ketiga untuk menghitung peta Voronoi dari sekumpulan situs (jika perlu).

Ini adalah kontes popularitas, jadi jawabannya dengan suara terbanyak menang. Pemilih didorong untuk menilai jawaban oleh

  • seberapa baik gambar asli dan warnanya diperkirakan.
  • seberapa baik algoritma bekerja pada berbagai jenis gambar.
  • seberapa baik algoritma ini bekerja untuk N kecil .
  • apakah algoritma secara adaptif mengelompokkan titik-titik di wilayah gambar yang memerlukan lebih detail.

Gambar Uji

Berikut adalah beberapa gambar untuk menguji algoritme Anda (beberapa tersangka biasa kami, beberapa yang baru). Klik gambar untuk versi yang lebih besar.

Gelombang Besar landak Pantai Cornell Saturnus Beruang coklat Yoshi Mandrill Nebula kepiting Kid Geobits Air terjun Berteriak

Pantai di baris pertama ditarik oleh Olivia Bell , dan termasuk dengan izinnya.

Jika Anda menginginkan tantangan ekstra, cobalah Yoshi dengan latar belakang putih dan luruskan perutnya.

Anda dapat menemukan semua gambar uji ini di galeri imgur ini di mana Anda dapat mengunduh semuanya sebagai file zip. Album ini juga berisi diagram Voronoi acak sebagai tes lain. Untuk referensi, berikut adalah data yang menghasilkannya .

Harap sertakan diagram contoh untuk berbagai gambar dan N yang berbeda , misalnya 100, 300, 1000, 3000 (serta pastebins ke beberapa spesifikasi sel yang sesuai). Anda dapat menggunakan atau menghilangkan tepi hitam di antara sel yang Anda inginkan (ini mungkin terlihat lebih baik pada beberapa gambar daripada yang lain). Jangan sertakan situsnya (kecuali dalam contoh terpisah, mungkin jika Anda ingin menjelaskan cara kerja penempatan situs Anda, tentu saja).

Jika Anda ingin menunjukkan sejumlah besar hasil, Anda dapat membuat galeri di imgur.com , agar ukuran jawaban tetap masuk akal. Atau, letakkan gambar kecil di pos Anda dan buat tautan ke gambar yang lebih besar, seperti yang saya lakukan pada jawaban referensi saya . Anda bisa mendapatkan thumbnail kecil dengan menambahkan snama file di tautan imgur.com (mis. I3XrT.png-> I3XrTs.png). Juga, jangan ragu untuk menggunakan gambar uji lain, jika Anda menemukan sesuatu yang bagus.

Renderer

Tempelkan output Anda ke dalam Stack Snippet berikut untuk membuat hasil Anda. Format daftar yang tepat tidak relevan, selama setiap sel ditentukan oleh 5 angka floating point dalam urutan x y r g b, di mana xdan ymerupakan koordinat situs sel, dan r g bmerupakan saluran warna merah, hijau dan biru dalam kisaran 0 ≤ r, g, b ≤ 1.

Cuplikan menyediakan opsi untuk menentukan lebar garis tepi sel, dan apakah situs sel harus ditampilkan atau tidak (yang terakhir sebagian besar untuk tujuan debugging). Tetapi perhatikan bahwa output hanya dirender ulang ketika spesifikasi sel berubah - jadi jika Anda memodifikasi beberapa opsi lain, tambahkan spasi ke sel atau sesuatu.

Penghargaan besar kepada Raymond Hill untuk menulis perpustakaan JS Voronoi yang sangat bagus ini .

Tantangan Terkait


5
@frogeyedpeas Dengan melihat suara yang Anda dapatkan. ;) Ini adalah kontes popularitas. Belum tentu ada yang cara terbaik untuk melakukannya. Idenya adalah Anda mencoba melakukannya sebaik mungkin, dan suara akan mencerminkan apakah orang setuju bahwa Anda telah melakukan pekerjaan dengan baik. Memang ada sejumlah subjektivitas dalam hal ini. Lihatlah tantangan terkait yang saya tautkan, atau yang ini . Anda akan melihat bahwa biasanya ada berbagai pendekatan tetapi sistem pemungutan suara membantu solusi yang lebih baik melambung ke atas dan memutuskan pemenang.
Martin Ender

3
Olivia menyetujui perkiraan pantai yang diajukan sejauh ini.
Alex A.

3
@AlexA. Devon menyetujui beberapa perkiraan wajahnya yang diajukan sejauh ini. Dia bukan penggemar berat dari salah satu versi n = 100;)
Geobits

1
@ Geobits: Dia akan mengerti kapan dia lebih tua.
Alex A.

1
Berikut adalah halaman tentang teknik penetapan berbasis voronoi centroidal . Sumber inspirasi yang baik (tesis master terkait memiliki diskusi yang bagus tentang kemungkinan peningkatan algoritma).
Pekerjaan

Jawaban:


112

Python + scipy + scikit-image , pengambilan sampel disk Poisson tertimbang

Solusi saya agak rumit. Saya melakukan beberapa preprocessing pada gambar untuk menghilangkan noise dan mendapatkan pemetaan seberapa 'menarik' setiap titik (menggunakan kombinasi entropi lokal dan deteksi tepi):

Kemudian saya memilih titik pengambilan sampel menggunakan pengambilan sampel cakram Poisson dengan twist: jarak lingkaran ditentukan oleh berat yang kami tentukan sebelumnya.

Kemudian setelah saya memiliki titik pengambilan sampel, saya membagi gambar dalam segmen voronoi dan menetapkan rata-rata L * a * b * dari nilai warna di dalam setiap segmen untuk setiap segmen.

Saya memiliki banyak heuristik, dan saya juga harus melakukan sedikit matematika untuk memastikan jumlah titik sampel dekat N. Saya mendapatkan Npersis dengan overshooting sedikit , dan kemudian menjatuhkan beberapa poin dengan heuristik.

Dalam hal runtime, filter ini tidak murah , tetapi tidak ada gambar di bawah ini yang membutuhkan waktu lebih dari 5 detik.

Tanpa basa-basi:

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!")

Gambar-gambar

Masing N- masing adalah 100, 300, 1000 dan 3000:

abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc


2
Saya suka ini; sepertinya kaca asap.
BobTheAwesome

3
Saya bermain-main dengan ini sedikit, dan Anda mendapatkan hasil yang lebih baik, terutama untuk gambar segitiga rendah, jika Anda mengganti denoise_bilatteral dengan denoise_tv_bregman. Ini menghasilkan lebih banyak patch bahkan dalam denoising, yang membantu.
LKlevin

@LKlevin Berapa berat yang Anda gunakan?
orlp

Saya menggunakan 1.0 sebagai bobot.
LKlevin

65

C ++

Pendekatan saya cukup lambat, tapi saya sangat senang dengan kualitas hasil yang diberikannya, terutama yang berkaitan dengan menjaga tepi. Misalnya, inilah Yoshi dan Kotak Cornell dengan masing-masing hanya 1000 situs:

Ada dua bagian utama yang membuatnya dicentang. Yang pertama, yang diwujudkan dalam evaluate()fungsi mengambil satu set lokasi kandidat situs, menetapkan warna optimal pada mereka dan mengembalikan skor untuk PSNR dari tessellation Voronoi yang diberikan versus gambar target. Warna untuk setiap situs ditentukan oleh rata-rata piksel gambar target yang dicakup oleh sel di sekitar situs. Saya menggunakan algoritma Welford untuk membantu menghitung baik warna terbaik untuk setiap sel dan hasil PSNR menggunakan hanya satu melewati gambar dengan memanfaatkan hubungan antara varians, MSE, dan PSNR. Ini mengurangi masalah menjadi salah satu dari menemukan set lokasi situs terbaik tanpa memperhatikan warna.

Bagian kedua, yang terkandung dalam main(), mencoba menemukan set ini. Dimulai dengan memilih satu set poin secara acak. Kemudian, di setiap langkah itu menghapus satu titik (round-robin) dan menguji satu set poin kandidat acak untuk menggantinya. Yang menghasilkan PSNR tertinggi dari kelompok itu diterima dan disimpan. Secara efektif, ini menyebabkan situs melompat ke lokasi baru dan umumnya meningkatkan gambar sedikit demi sedikit. Perhatikan bahwa algoritma ini sengaja tidak mempertahankan lokasi asli sebagai kandidat. Terkadang ini berarti lompatan menurunkan kualitas gambar keseluruhan. Membiarkan ini terjadi membantu menghindari terjebak dalam maksima lokal. Ini juga memberikan kriteria berhenti; program berakhir setelah melakukan sejumlah langkah tanpa meningkatkan set situs terbaik yang ditemukan sejauh ini.

Perhatikan bahwa implementasi ini cukup mendasar dan dapat dengan mudah menghabiskan waktu berjam-jam untuk CPU-core, terutama ketika jumlah situs bertambah. Ini mengkompilasi ulang peta Voronoi lengkap untuk setiap kandidat dan brute force menguji jarak ke semua situs untuk setiap piksel. Karena setiap operasi melibatkan penghapusan satu titik pada satu waktu dan menambahkan yang lain, perubahan sebenarnya pada gambar pada setiap langkah akan menjadi cukup lokal. Ada algoritma untuk secara efisien memperbarui diagram Voronoi dan saya percaya mereka akan memberikan algoritma ini percepatan yang luar biasa. Untuk kontes ini, bagaimanapun, saya telah memilih untuk menjaga hal-hal sederhana dan kasar.

Kode

#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;
 }

Lari

Program ini mandiri dan tidak memiliki dependensi eksternal di luar perpustakaan standar, tetapi memang membutuhkan gambar dalam format PPM biner . Saya menggunakan ImageMagick untuk mengonversi gambar ke PPM, meskipun GIMP dan beberapa program lain juga dapat melakukannya.

Untuk mengkompilasinya, simpan program sebagai voronoi.cppdan kemudian jalankan:

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

Saya berharap ini mungkin akan bekerja pada Windows dengan versi terbaru dari Visual Studio, meskipun saya belum mencoba ini. Anda ingin memastikan bahwa Anda sedang mengompilasi dengan C ++ 11 atau lebih baik dan OpenMP diaktifkan jika Anda melakukannya. OpenMP tidak sepenuhnya diperlukan, tetapi banyak membantu dalam membuat waktu pelaksanaan lebih dapat ditoleransi.

Untuk menjalankannya, lakukan sesuatu seperti:

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

File yang lebih baru akan diperbarui dengan data situs saat berjalan. Baris pertama akan memiliki lebar dan tinggi gambar, diikuti oleh garis nilai x, y, r, g, b yang cocok untuk menyalin dan menempel ke penyaji Javascript dalam deskripsi masalah.

Tiga konstanta di bagian atas program memungkinkan Anda menyetelnya untuk kecepatan versus kualitas. The decimationFaktor coarsens gambar target ketika mengevaluasi satu set situs untuk warna dan PSNR. Semakin tinggi, semakin cepat program akan berjalan. Mengaturnya ke 1 menggunakan gambar resolusi penuh. The candidateskontrol konstan berapa banyak kandidat untuk menguji pada setiap langkah. Lebih tinggi memberikan kesempatan lebih baik untuk menemukan tempat yang baik untuk melompat tetapi membuat program lebih lambat. Terakhir, terminationadalah berapa banyak langkah yang dapat dilakukan program tanpa meningkatkan outputnya sebelum berhenti. Meningkatkannya mungkin memberikan hasil yang lebih baik tetapi membuatnya sedikit lebih lama.

Gambar-gambar

N = 100, 300, 1000, dan 3000:


1
Ini seharusnya memenangkan IMO - jauh lebih baik daripada milikku.
orlp

1
@orlp - Terima kasih! Agar adil, Anda memposting Anda lebih cepat dan berjalan jauh lebih cepat. Kecepatan menghitung!
Boojum

1
Nah, saya tidak benar-benar jawaban peta voronoi :) Ini algoritma pengambilan sampel benar-benar baik, tapi mengubah titik sampel ke situs voronoi jelas tidak optimal.
orlp

55

IDL, penyempurnaan adaptif

Metode ini terinspirasi oleh Penyempurnaan Mesh Adaptif dari simulasi astronomi, dan juga permukaan Pembagian . Ini adalah jenis tugas yang dibanggakan IDL sendiri, yang dapat Anda ketahui dari sejumlah besar fungsi bawaan yang dapat saya gunakan. : D

Saya sudah menampilkan beberapa perantara untuk gambar uji yoshi latar belakang hitam, dengan n = 1000.

Pertama, kami melakukan skala abu-abu pencahayaan pada gambar (menggunakan ct_luminance), dan menerapkan filter Prewitt ( prewitt, lihat wikipedia ) untuk deteksi tepi yang baik:

abc abc

Kemudian muncul pekerjaan kasar yang sebenarnya: kita membagi gambar menjadi 4, dan mengukur varians di setiap kuadran dalam gambar yang difilter. Varians kami tertimbang oleh ukuran subdivisi (yang pada langkah pertama ini sama), sehingga daerah "edgy" dengan varians tinggi tidak dapat dibagi lagi menjadi lebih kecil dan lebih kecil dan lebih kecil. Kemudian, kami menggunakan varian tertimbang untuk menargetkan subdivisi dengan lebih detail, dan secara iteratif membagi masing-masing bagian yang kaya detail menjadi 4 tambahan, hingga kami mencapai jumlah target situs kami (setiap subdivisi berisi tepat satu situs). Karena kami menambahkan 3 situs setiap kali kami melakukan iterate, kami berakhir dengan n - 2 <= N <= nsitus.

Saya membuat .webm dari proses pembagian untuk gambar ini, yang tidak dapat saya embed, tapi ini ada di sini ; warna di setiap subbagian ditentukan oleh varian tertimbang. (Saya membuat jenis video yang sama untuk yoshi latar belakang putih, untuk perbandingan, dengan tabel warna terbalik sehingga mengarah ke putih, bukan hitam; itu di sini .) Produk akhir dari subdivisi terlihat seperti ini:

abc

Setelah kami memiliki daftar subdivisi kami, kami pergi melalui masing-masing subdivisi. Lokasi situs terakhir adalah posisi minimum gambar Prewitt, yaitu, piksel "paling tidak edgy", dan warna bagian adalah warna piksel itu; inilah gambar asli, dengan situs ditandai:

abc

Kemudian, kami menggunakan built-in triangulateuntuk menghitung triangulasi Delaunay situs, dan built-in voronoiuntuk menentukan simpul dari setiap poligon Voronoi, sebelum menggambar setiap poligon ke buffer gambar dalam warna masing-masing. Akhirnya, kami menyimpan snapshot dari buffer gambar.

abc

Kode:

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

Sebut ini via voro_map, n, image, output_filename. Saya juga menyertakan wrapperprosedur, yang melewati setiap gambar uji dan dijalankan dengan 100, 500, dan 1000 situs.

Output dikumpulkan di sini , dan berikut adalah beberapa thumbnail:

n = 100

abc abc abc abc abc abc abc abc abc abc abc abc abc

n = 500

abc abc abc abc abc abc abc abc abc abc abc abc abc

n = 1000

abc abc abc abc abc abc abc abc abc abc abc abc abc


9
Saya sangat menyukai kenyataan bahwa solusi ini menempatkan lebih banyak poin di area yang lebih kompleks, yang saya pikir maksudnya, dan membedakannya dari yang lain pada saat ini.
alexander-brett

ya, gagasan tentang poin-poin yang dikelompokkan secara detail inilah yang mendorong saya ke penyempurnaan adaptif
sirpercival

3
Penjelasan yang sangat rapi, dan gambar-gambarnya mengesankan! Saya punya pertanyaan - Sepertinya Anda mendapatkan gambar yang jauh berbeda ketika Yoshi berada di latar belakang putih, di mana kami memiliki beberapa bentuk aneh. Apa yang menyebabkannya?
BrainSteel

2
@BrianSteel Saya membayangkan garis dijemput sebagai daerah tinggi varians dan terfokus pada tidak perlu, dan kemudian lainnya benar-benar tinggi daerah rinci memiliki poin lebih sedikit ditugaskan karena itu.
doppelgreener

@BrainSteel saya pikir doppel benar - ada keunggulan kuat antara perbatasan hitam dan latar belakang putih, yang meminta banyak detail dalam algoritma. saya tidak yakin apakah ini sesuatu yang saya dapat (atau, lebih penting lagi, harus ) perbaiki ...
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)

Algoritma

Gagasan intinya adalah bahwa k-means mengelompokkan secara alami partisi gambar ke dalam sel Voronoi, karena titik-titik terkait dengan centroid terdekat. Namun, kita perlu menambahkan warna sebagai kendala.

Pertama-tama kita mengonversi setiap piksel ke ruang warna Lab , untuk manipulasi warna yang lebih baik.

Lalu kami melakukan semacam "deteksi tepi orang miskin". Untuk setiap piksel, kita melihat tetangganya yang ortogonal dan diagonal, dan menghitung perbedaan rata-rata kuadrat dalam warna. Kami kemudian mengurutkan semua piksel berdasarkan perbedaan ini, dengan piksel yang paling mirip dengan tetangga mereka di bagian depan daftar, dan piksel yang paling berbeda dengan tetangga mereka di belakang (yaitu, lebih cenderung menjadi titik tepi). Berikut adalah contoh untuk planet ini, di mana semakin terang pikselnya, semakin berbeda dari tetangganya:

masukkan deskripsi gambar di sini

(Ada pola kisi-kisi yang jelas pada output yang diberikan di atas. Menurut @randomra, ini mungkin disebabkan oleh enkode JPG yang hilang, atau imgur mengompresi gambar.)

Selanjutnya, kami menggunakan pemesanan piksel ini untuk mengambil sampel sejumlah besar titik yang akan dikelompokkan. Kami menggunakan distribusi eksponensial, memberikan prioritas pada poin yang lebih mirip tepi dan "menarik".

masukkan deskripsi gambar di sini

Untuk pengelompokan, kami pertama-tama memilih Ncentroid, yang dipilih secara acak menggunakan distribusi eksponensial yang sama seperti di atas. Iterasi awal dilakukan, dan untuk masing-masing cluster yang dihasilkan, kami menetapkan warna rata-rata dan ambang batas varian warna. Kemudian untuk sejumlah iterasi, kami:

  • Bangun triangulasi centroid dari Delaunay, sehingga kita dapat dengan mudah meminta tetangga untuk centroid.
  • Gunakan triangulasi untuk menghilangkan centroid yang warnanya hampir sama dengan sebagian besar (> 4/5) tetangga mereka dan tetangga tetangga digabungkan. Setiap titik sampel terkait juga dihapus, dan centroid pengganti baru dan titik sampel ditambahkan. Langkah ini mencoba untuk memaksa algoritma untuk menempatkan lebih banyak cluster di mana dibutuhkan detail.
  • Buat kd-tree untuk centroid baru, sehingga kita dapat dengan mudah meminta centroid terdekat ke titik sampel mana pun.
  • Gunakan pohon untuk menetapkan setiap titik sampel ke salah satu dari 6 centroid terdekat (6 dipilih secara empiris). Centroid hanya akan menerima titik sampel jika warna titik berada dalam ambang varians warna centroid. Kami mencoba untuk menetapkan setiap titik sampel ke centroid penerima pertama, tetapi jika itu tidak memungkinkan maka kami hanya menetapkannya ke centroid terdekat. "Ketidakjelasan" algoritma berasal dari langkah ini, karena kluster dimungkinkan untuk tumpang tindih.
  • Hitung ulang centroid.

masukkan deskripsi gambar di sini

(Klik untuk ukuran penuh)

Akhirnya, kami mencicipi sejumlah besar poin, kali ini menggunakan distribusi yang seragam. Menggunakan kd-tree lain, kami menetapkan setiap titik ke pusat massa terdekat, membentuk kelompok. Kami kemudian memperkirakan warna median dari masing-masing cluster menggunakan algoritma mendaki bukit, memberikan warna sel terakhir kami (ide untuk langkah ini berkat @PhiNotPi dan @ MartinBüttner).

masukkan deskripsi gambar di sini

Catatan

Selain mengeluarkan file teks untuk snippet ( OUTFILE), jika DEBUGdiatur ke Trueprogram juga akan menampilkan dan menimpa gambar di atas. Algoritme membutuhkan beberapa menit untuk setiap gambar, jadi ini adalah cara yang baik untuk memeriksa kemajuan yang tidak menambah banyak waktu berjalan.

Output sampel

N = 32:

masukkan deskripsi gambar di sini

N = 100:

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

N = 1000:

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

N = 3000:

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini


1
Saya sangat suka seberapa baik Yoshis putih Anda ternyata.
Maks

26

Mathematica, Sel Acak

Ini adalah solusi dasar, jadi Anda mendapatkan gagasan tentang minimum yang saya minta dari Anda. Diberi nama file (lokal atau sebagai URL) difile dan N di n, kode berikut hanya memilih N piksel acak, dan menggunakan warna yang ditemukan di piksel tersebut. Ini benar-benar naif dan tidak bekerja dengan baik, tetapi saya ingin kalian mengalahkan ini semua. :)

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

Berikut ini semua gambar uji untuk N = 100 (semua gambar terhubung ke versi yang lebih besar):

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Seperti yang Anda lihat, ini pada dasarnya tidak berguna. Meskipun mereka mungkin memiliki nilai artistik, dengan cara ekspresionis, gambar asli hampir tidak dikenali.

Untuk N = 500 , situasinya sedikit ditingkatkan, tetapi masih ada artefak yang sangat aneh, gambar terlihat pudar, dan banyak sel yang terbuang pada daerah tanpa detail:

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Giliranmu!


Saya bukan pembuat kode yang baik tetapi tuhan gambar-gambar ini tampak cantik. Ide yang luar biasa!
Faraz Masroor

Ada alasan Dimensions@ImageDatadan tidak ImageDimensions? Anda dapat menghindari yang lambat ImageDatasama sekali dengan menggunakan PixelValue.
Arcampion

@ 2012rcampion Tidak ada alasan, saya hanya tidak tahu kedua fungsi itu ada. Saya mungkin memperbaikinya nanti dan juga mengubah contoh gambar ke nilai-N yang disarankan.
Martin Ender

23

Mathematica

Kita semua tahu Martin mencintai Mathematica, jadi izinkan saya mencobanya dengan Mathematica.

Algoritme saya menggunakan titik acak dari tepi gambar untuk membuat diagram awal voronoi. Diagram kemudian diprettify oleh penyesuaian berulang mesh dengan filter rata-rata sederhana. Ini memberikan gambar dengan kepadatan sel tinggi di dekat daerah kontras tinggi dan sel menyenangkan secara visual tanpa sudut gila.

Gambar-gambar berikut menunjukkan contoh proses yang sedang berjalan. Kesenangannya agak dimanjakan oleh Mathematicas Antialiasing yang buruk, tapi kami mendapatkan grafis vektor, itu pasti berharga.

Algoritma ini, tanpa pengambilan sampel acak, dapat ditemukan dalam VoronoiMeshdokumentasi di sini .

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Gambar Uji (100,300,1000,3000)

Kode

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]}]]
   ];

Pekerjaan yang bagus untuk posting pertama! :) Anda mungkin ingin mencoba tes gambar Voronoi dengan 32 sel (itu adalah jumlah sel dalam gambar itu sendiri).
Martin Ender

Terima kasih! Saya kira algoritma saya akan tampil sangat buruk pada contoh ini. Benih akan diinisialisasi pada tepi sel dan rekursi tidak akan membuatnya lebih baik;)
cakar

Meskipun laju lebih lambat untuk menyatu dengan gambar asli, saya menemukan bahwa algoritma Anda menghasilkan hasil yang sangat artistik! (seperti versi perbaikan karya Georges Seurat). Kerja bagus!
neizod

Anda juga bisa mendapatkan warna poligon interpolasi yang tampak seperti kaca dengan mengubah garis akhir Anda keGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
Histogram

13

Pembawa acara Python + SciPy +

Algoritma yang saya gunakan adalah sebagai berikut:

  1. Ubah ukuran gambar ke ukuran lebih kecil (~ 150 piksel)
  2. Buat gambar unsharp-masked dari nilai maksimum saluran (ini membantu tidak mengambil wilayah putih terlalu kuat).
  3. Ambil nilai absolut.
  4. Pilih titik acak dengan probabilitas yang sebanding dengan gambar ini. Ini memilih poin dari kedua sisi diskontinuitas.
  5. Perbaiki poin yang dipilih untuk menurunkan fungsi biaya. Fungsi ini adalah jumlah maksimum dari penyimpangan kuadrat dalam saluran (sekali lagi membantu bias ke warna solid dan tidak hanya putih solid). Saya telah menyalahgunakan Markov Chain Monte Carlo dengan modul pembawa acara (sangat disarankan) sebagai pengoptimal. Prosedur ini menyelamatkan ketika tidak ada perbaikan baru ditemukan setelah iterasi rantai N.

Algoritme tampaknya berfungsi dengan sangat baik. Sayangnya itu hanya bisa berjalan dengan masuk akal pada gambar yang bertubuh kecil. Saya belum punya waktu untuk mengambil poin Voronoi dan menerapkannya pada gambar yang lebih besar. Mereka dapat disempurnakan pada saat ini. Saya juga bisa menjalankan MCMC lebih lama untuk mendapatkan minima yang lebih baik. Titik lemah algoritme adalah bahwa itu agak mahal. Saya belum punya waktu untuk meningkatkan lebih dari 1000 poin dan beberapa gambar 1000 poin sebenarnya masih disempurnakan.

(klik kanan dan lihat gambar untuk mendapatkan versi yang lebih besar)

Thumbnail untuk 100, 300 dan 1000 poin

Tautan ke versi yang lebih besar adalah http://imgur.com/a/2IXDT#9 (100 poin), http://imgur.com/a/bBQ7q (300 poin) dan http://imgur.com/a/rr8wJ (1000 poin)

#!/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()

Gambar yang tidak tertutup topeng terlihat seperti berikut ini. Poin acak dipilih dari gambar jika angka acak kurang dari nilai gambar (normed ke 1):

Gambar Saturnus bertopeng dicabut

Saya akan memposting gambar yang lebih besar dan poin Voronoi jika saya mendapatkan lebih banyak waktu.

Sunting: Jika Anda menambah jumlah walker menjadi 100 * npts, ubah fungsi biaya menjadi beberapa kuadrat dari penyimpangan di semua saluran, dan tunggu beberapa saat (meningkatkan jumlah iterasi untuk keluar dari loop menjadi 200), dimungkinkan untuk membuat beberapa gambar yang bagus hanya dengan 100 poin:

Gambar 11, 100 poin Gambar 2, 100 poin Gambar 4, 100 poin Gambar 10, 100 poin


3

Menggunakan energi gambar sebagai peta titik-berat

Dalam pendekatan saya terhadap tantangan ini, saya ingin cara untuk memetakan "relevansi" area gambar tertentu dengan probabilitas bahwa suatu titik tertentu akan dipilih sebagai centroid Voronoi. Namun, saya masih ingin melestarikan nuansa artistik Voronoi mosaicing dengan memilih titik gambar secara acak. Selain itu, saya ingin beroperasi pada gambar besar, jadi saya tidak kehilangan apa pun dalam proses downsampling. Algoritme saya kira-kira seperti ini:

  1. Untuk setiap gambar, buat peta ketajaman. Peta ketajaman didefinisikan oleh energi gambar yang dinormalisasi (atau kuadrat dari sinyal frekuensi tinggi dari gambar). Contohnya terlihat seperti ini:

Peta ketajaman

  1. Hasilkan sejumlah titik dari gambar, ambil 70 persen dari titik-titik dalam peta ketajaman dan 30 persen dari semua titik lainnya. Ini berarti bahwa titik-titik diambil sampelnya lebih padat dari bagian detail gambar yang tinggi.
  2. Warna!

Hasil

N = 100, 500, 1000, 3000

Gambar 1, N = 100 Gambar 1, N = 500 Gambar 1, N = 1000 Gambar 1, N = 3000

Gambar 2, N = 100 Gambar 2, N = 500 Gambar 2, N = 1000 Gambar 2, N = 3000

Gambar 3, N = 100 Gambar 3, N = 500 Gambar 3, N = 1000 Gambar 3, N = 3000

Gambar 4, N = 100 Gambar 4, N = 500 Gambar 4, N = 1000 Gambar 4, N = 3000

Gambar 5, N = 100 Gambar 5, N = 500 Gambar 5, N = 1000 Gambar 5, N = 3000

Gambar 6, N = 100 Gambar 6, N = 500 Gambar 6, N = 1000 Gambar 6, N = 3000

Gambar 7, N = 100 Gambar 7, N = 500 Gambar 7, N = 1000 Gambar 7, N = 3000

Gambar 8, N = 100 Gambar 8, N = 500 Gambar 8, N = 1000 Gambar 8, N = 3000

Gambar 9, N = 100 Gambar 9, N = 500 Gambar 9, N = 1000 Gambar 9, N = 3000

Gambar 10, N = 100 Gambar 10, N = 500 Gambar 10, N = 1000 Gambar 10, N = 3000

Gambar 11, N = 100 Gambar 11, N = 500 Gambar 11, N = 1000 Gambar 11, N = 3000

Gambar 12, N = 100 Gambar 12, N = 500 Gambar 12, N = 1000 Gambar 12, N = 3000

Gambar 13, N = 100 Gambar 13, N = 500 Gambar 13, N = 1000 Gambar 13, N = 3000

Gambar 14, N = 100 Gambar 14, N = 500 Gambar 14, N = 1000 Gambar 14, N = 3000


14
Apakah Anda keberatan a) termasuk kode sumber yang digunakan untuk menghasilkan ini, dan b) menautkan setiap thumbnail ke gambar ukuran penuh?
Martin Ender
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.