Vẽ một hình ảnh dưới dạng bản đồ Voronoi


170

Tín dụng cho Sở thích của Calvin để thúc đẩy ý tưởng thách thức của tôi đi đúng hướng.

Hãy xem xét một tập hợp các điểm trong mặt phẳng mà chúng ta sẽ gọi các trang web và liên kết một màu với mỗi trang web. Bây giờ bạn có thể vẽ toàn bộ mặt phẳng bằng cách tô màu từng điểm bằng màu của trang web gần nhất. Đây được gọi là bản đồ Voronoi (hoặc sơ đồ Voronoi ). Về nguyên tắc, bản đồ Voronoi có thể được xác định cho bất kỳ số liệu khoảng cách nào, nhưng chúng ta chỉ cần sử dụng khoảng cách Euclide thông thường r = √(x² + y²). ( Lưu ý: Bạn không nhất thiết phải biết cách tính toán và kết xuất một trong số này để cạnh tranh trong thử thách này.)

Dưới đây là một ví dụ với 100 trang web:

nhập mô tả hình ảnh ở đây

Nếu bạn nhìn vào bất kỳ ô nào, thì tất cả các điểm trong ô đó sẽ gần với trang tương ứng hơn bất kỳ trang nào khác.

Nhiệm vụ của bạn là ước tính một hình ảnh nhất định với bản đồ Voronoi như vậy. Bạn đang đưa ra những hình ảnh trong bất kỳ định dạng đồ họa raster thuận tiện, cũng như một số nguyên N . Sau đó, bạn nên tạo tối đa N trang web và màu sắc cho mỗi trang web, sao cho bản đồ Voronoi dựa trên các trang web này giống với hình ảnh đầu vào càng sát càng tốt.

Bạn có thể sử dụng Stack Snippet ở cuối thử thách này để hiển thị bản đồ Voronoi từ đầu ra của bạn hoặc bạn có thể tự kết xuất nó nếu muốn.

Bạn có thể sử dụng các hàm tích hợp hoặc của bên thứ ba để tính toán bản đồ Voronoi từ một tập hợp các trang web (nếu bạn cần).

Đây là một cuộc thi phổ biến, vì vậy câu trả lời có nhiều lượt bình chọn nhất sẽ thắng. Cử tri được khuyến khích đánh giá câu trả lời bằng cách

  • hình ảnh gốc và màu sắc của chúng gần đúng như thế nào.
  • thuật toán hoạt động tốt như thế nào trên các loại hình ảnh khác nhau.
  • thuật toán hoạt động tốt như thế nào đối với N nhỏ .
  • thuật toán có thích ứng cụm điểm trong các khu vực của hình ảnh đòi hỏi chi tiết hơn không.

Hình ảnh thử nghiệm

Dưới đây là một vài hình ảnh để kiểm tra thuật toán của bạn (một số nghi phạm thông thường của chúng tôi, một số hình ảnh mới). Nhấp vào hình ảnh cho các phiên bản lớn hơn.

Sóng lớn nhím bờ biển Cornell sao Thổ Gấu nâu Yoshi Mandrill Tinh vân cua Đứa trẻ của Geobits Thác nước Hét lên

Bãi biển ở hàng đầu tiên được vẽ bởi Olivia Bell , và có sự cho phép của cô.

Nếu bạn muốn có thêm một thử thách, hãy thử Yoshi với nền trắng và có được đường bụng của anh ấy ngay.

Bạn có thể tìm thấy tất cả những hình ảnh thử nghiệm này trong bộ sưu tập imgur này , nơi bạn có thể tải xuống tất cả chúng dưới dạng tệp zip. Album cũng chứa một sơ đồ Voronoi ngẫu nhiên như một thử nghiệm khác. Để tham khảo, đây là dữ liệu tạo ra nó .

Vui lòng bao gồm các sơ đồ ví dụ cho nhiều hình ảnh khác nhau và N , ví dụ 100, 300, 1000, 3000 (cũng như các pastebins cho một số thông số kỹ thuật của ô tương ứng). Bạn có thể sử dụng hoặc bỏ qua các cạnh màu đen giữa các ô khi bạn thấy phù hợp (điều này có thể trông tốt hơn trên một số hình ảnh so với các hình ảnh khác). Không bao gồm các trang mặc dù (ngoại trừ trong một ví dụ riêng biệt có thể nếu bạn muốn giải thích cách vị trí trang web của bạn hoạt động, tất nhiên).

Nếu bạn muốn hiển thị một số lượng lớn kết quả, bạn có thể tạo một bộ sưu tập trên imgur.com , để giữ cho kích thước của các câu trả lời hợp lý. Hoặc, đặt hình thu nhỏ trong bài đăng của bạn và làm cho chúng liên kết đến hình ảnh lớn hơn, như tôi đã làm trong câu trả lời tham khảo của mình . Bạn có thể nhận được các hình thu nhỏ bằng cách thêm svào tên tệp trong liên kết imgur.com (ví dụ: I3XrT.png> I3XrTs.png). Ngoài ra, hãy thoải mái sử dụng các hình ảnh thử nghiệm khác, nếu bạn tìm thấy một cái gì đó tốt đẹp.

Trình kết xuất

Dán đầu ra của bạn vào Stack Snippet sau để hiển thị kết quả của bạn. Định dạng danh sách chính xác là không liên quan, miễn là mỗi ô được chỉ định bởi 5 số dấu phẩy động theo thứ tự x y r g b, trong đó xylà tọa độ của trang web của ô và r g blà các kênh màu đỏ, lục và lam trong phạm vi 0 ≤ r, g, b ≤ 1.

Đoạn mã cung cấp các tùy chọn để chỉ định độ rộng dòng của các cạnh của ô và liệu các trang web di động có được hiển thị hay không (phần sau chủ yếu cho mục đích gỡ lỗi). Nhưng lưu ý rằng đầu ra chỉ được kết xuất lại khi thông số kỹ thuật của ô thay đổi - vì vậy nếu bạn sửa đổi một số tùy chọn khác, hãy thêm khoảng trắng vào các ô hoặc thứ gì đó.

Các khoản tín dụng lớn cho Raymond Hill để viết thư viện JS Voronoi thực sự tốt đẹp này .

Những thách thức liên quan


5
@frogeyedpeas Bằng cách nhìn vào số phiếu bạn nhận được. ;) Đây là một cuộc thi phổ biến. Không nhất thiết phải cách tốt nhất để làm. Ý tưởng là bạn cố gắng làm tốt nhất có thể, và phiếu bầu sẽ phản ánh xem mọi người có đồng ý rằng bạn đã hoàn thành tốt công việc hay không. Có một số lượng chủ quan nhất định trong những điều này, thừa nhận. Hãy nhìn vào những thách thức liên quan mà tôi liên kết, hoặc tại cái này . Bạn sẽ thấy rằng thường có nhiều cách tiếp cận khác nhau nhưng hệ thống bỏ phiếu giúp các giải pháp tốt hơn nổi lên hàng đầu và quyết định người chiến thắng.
Martin Ender

3
Olivia chấp thuận các xấp xỉ của bãi biển của cô gửi cho đến nay.
Alex A.

3
@AlexA. Devon chấp thuận một số xấp xỉ của khuôn mặt của mình cho đến nay. Anh ấy không phải là một fan hâm mộ lớn của bất kỳ phiên bản n = 100 nào;)
Geobits 18/05/2015

1
@Geobits: Anh ấy sẽ hiểu khi anh ấy già đi.
Alex A.

1
Đây là một trang về một kỹ thuật kê đơn dựa trên voronoi trung tâm . Một nguồn cảm hứng tốt (luận văn thạc sĩ liên quan có một cuộc thảo luận tốt đẹp về các cải tiến có thể cho thuật toán).
Công việc

Câu trả lời:


112

Python + scipy + scikit-image , lấy mẫu đĩa Poisson có trọng số

Giải pháp của tôi khá phức tạp. Tôi thực hiện một số tiền xử lý trên hình ảnh để loại bỏ nhiễu và có được ánh xạ mức độ "thú vị" của từng điểm (sử dụng kết hợp entropy cục bộ và phát hiện cạnh):

Sau đó, tôi chọn các điểm lấy mẫu bằng cách sử dụng lấy mẫu đĩa Poisson bằng một vòng xoắn: khoảng cách của vòng tròn được xác định bởi trọng lượng chúng tôi đã xác định trước đó.

Sau đó, khi tôi có các điểm lấy mẫu, tôi chia hình ảnh thành các phân đoạn voronoi và gán trung bình L * a * b * của các giá trị màu bên trong mỗi phân đoạn cho mỗi phân đoạn.

Tôi có rất nhiều heuristic, và tôi cũng phải làm một chút toán để đảm bảo số lượng điểm mẫu gần với N. Tôi nhận được Nchính xác bằng cách vượt quá một chút , và sau đó giảm một số điểm với một heuristic.

Về thời gian chạy, bộ lọc này không rẻ , nhưng không có hình ảnh nào dưới đây mất hơn 5 giây để thực hiện.

Nếu không có thêm rắc rối:

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

Hình ảnh

Tương ứng Nlà 100, 300, 1000 và 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
Tôi thích điều này; Nó trông hơi giống như thủy tinh hun khói.
BobTheAwgie 17/05/2015

3
Tôi đã loay hoay với điều này một chút và bạn sẽ nhận được kết quả tốt hơn, đặc biệt là đối với các hình ảnh tam giác thấp, nếu bạn thay thế denaty_bilatteral bằng denoir_tv_bregman. Nó tạo ra nhiều bản vá hơn trong việc khử nhiễu của nó, giúp.
LKlevin

@LKlevin Bạn đã sử dụng cân nặng nào?
orlp

Tôi đã sử dụng 1.0 làm trọng lượng.
LKlevin

65

C ++

Cách tiếp cận của tôi khá chậm, nhưng tôi rất hài lòng với chất lượng của kết quả mà nó mang lại, đặc biệt là liên quan đến việc giữ gìn các cạnh. Ví dụ: đây là YoshiCornell Box chỉ với 1000 trang web:

Có hai phần chính làm cho nó đánh dấu. Đầu tiên, được thể hiện trong evaluate()hàm lấy một tập hợp các vị trí trang web ứng cử viên, đặt màu tối ưu cho chúng và trả về điểm cho PSNR của phần hiển thị Voronoi được hiển thị so với hình ảnh mục tiêu. Màu sắc cho mỗi trang web được xác định bằng cách lấy trung bình các pixel hình ảnh mục tiêu được bao phủ bởi ô xung quanh trang web. Tôi sử dụng thuật toán của Welford để giúp tính toán cả màu sắc tốt nhất cho từng ô và PSNR kết quả bằng cách chỉ sử dụng một lần duy nhất trên hình ảnh bằng cách khai thác mối quan hệ giữa phương sai, MSE và PSNR. Điều này làm giảm vấn đề xuống một trong những tìm kiếm vị trí tốt nhất mà không liên quan đến màu sắc.

Phần thứ hai sau đó, được thể hiện trong main(), cố gắng tìm bộ này. Nó bắt đầu bằng cách chọn một tập hợp các điểm ngẫu nhiên. Sau đó, trong mỗi bước, nó sẽ loại bỏ một điểm (đi vòng tròn) và kiểm tra một tập hợp các điểm ứng cử viên ngẫu nhiên để thay thế nó. Một trong đó mang lại PSNR cao nhất của bó được chấp nhận và giữ. Thực tế, điều này làm cho trang web nhảy đến một vị trí mới và thường cải thiện hình ảnh từng chút một. Lưu ý rằng thuật toán cố ý không giữ lại vị trí ban đầu như một ứng cử viên. Đôi khi điều này có nghĩa là bước nhảy làm giảm chất lượng hình ảnh tổng thể. Cho phép điều này xảy ra giúp tránh bị mắc kẹt trong cực đại địa phương. Nó cũng đưa ra một tiêu chí dừng lại; chương trình chấm dứt sau khi thực hiện một số bước nhất định mà không cải thiện trên tập hợp các trang web tốt nhất được tìm thấy cho đến nay.

Lưu ý rằng việc triển khai này khá cơ bản và có thể dễ dàng mất hàng giờ thời gian lõi CPU, đặc biệt là khi số lượng trang web tăng lên. Nó tính toán lại bản đồ Voronoi hoàn chỉnh cho mọi ứng cử viên và lực lượng vũ phu kiểm tra khoảng cách đến tất cả các trang web cho mỗi pixel. Vì mỗi thao tác liên quan đến việc xóa một điểm tại một thời điểm và thêm một điểm khác, nên những thay đổi thực tế đối với hình ảnh ở mỗi bước sẽ khá cục bộ. Có các thuật toán để cập nhật hiệu quả một sơ đồ Voronoi và tôi tin rằng chúng sẽ giúp thuật toán này tăng tốc rất nhanh. Tuy nhiên, đối với cuộc thi này, tôi đã chọn giữ mọi thứ đơn giản và mạnh mẽ.

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

Đang chạy

Chương trình này độc lập và không có phụ thuộc bên ngoài ngoài thư viện tiêu chuẩn, nhưng nó yêu cầu hình ảnh phải ở định dạng PPM nhị phân . Tôi sử dụng ImageMagick để chuyển đổi hình ảnh sang PPM, mặc dù GIMP và khá nhiều chương trình khác cũng có thể làm điều đó.

Để biên dịch nó, lưu chương trình voronoi.cppvà sau đó chạy:

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

Tôi hy vọng nó có thể sẽ hoạt động trên Windows với các phiên bản Visual Studio gần đây, mặc dù tôi chưa thử điều này. Bạn sẽ muốn đảm bảo rằng bạn đang biên dịch với C ++ 11 hoặc tốt hơn và bật OpenMP nếu bạn làm như vậy. OpenMP không thực sự cần thiết, nhưng nó giúp ích rất nhiều trong việc thực hiện thời gian thực hiện dễ chịu hơn.

Để chạy nó, hãy làm một cái gì đó như:

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

Các tập tin sau sẽ được cập nhật với dữ liệu trang web khi nó đi. Dòng đầu tiên sẽ có chiều rộng và chiều cao của hình ảnh, theo sau là các dòng giá trị x, y, r, g, b phù hợp để sao chép và dán vào trình kết xuất Javascript trong mô tả sự cố.

Ba hằng số ở đầu chương trình cho phép bạn điều chỉnh tốc độ so với chất lượng. Các decimationyếu tố coarsens hình ảnh mục tiêu khi đánh giá một tập hợp các trang web cho màu sắc và PSNR. Nó càng cao, chương trình sẽ chạy càng nhanh. Đặt nó thành 1 sử dụng hình ảnh độ phân giải đầy đủ. Các candidatesđiều khiển liên tục bao nhiêu ứng cử viên để thử nghiệm trên từng bước. Cao hơn cho một cơ hội tốt hơn để tìm một vị trí tốt để nhảy đến nhưng làm cho chương trình chậm hơn. Cuối cùng, terminationlà bao nhiêu bước chương trình có thể đi mà không cải thiện đầu ra của nó trước khi thoát. Tăng nó có thể cho kết quả tốt hơn nhưng làm cho nó mất nhiều thời gian hơn.

Hình ảnh

N = 100, 300, 1000 và 3000:


1
Điều này đã giành được IMO - tốt hơn nhiều so với của tôi.
orlp

1
@orlp - Cảm ơn! Để công bằng, mặc dù bạn đã đăng của bạn sớm hơn nhiều và nó chạy nhanh hơn nhiều. Tốc độ đếm!
Boojum

1
Chà, tôi không thực sự là một câu trả lời bản đồ voronoi :) Đó là một thuật toán lấy mẫu thực sự tốt, nhưng biến các điểm mẫu thành các trang web voronoi rõ ràng là không tối ưu.
orlp

55

IDL, sàng lọc thích ứng

Phương pháp này được lấy cảm hứng từ Tinh chỉnh lưới thích ứng từ các mô phỏng thiên văn, và cả bề mặt phân khu . Đây là loại nhiệm vụ mà IDL tự hào, mà bạn sẽ có thể biết được bằng số lượng lớn các hàm dựng sẵn mà tôi có thể sử dụng. :CƯỜI MỞ MIỆNG

Tôi đã xuất một số sản phẩm trung gian cho hình ảnh thử nghiệm yoshi nền đen, với n = 1000.

Đầu tiên, chúng tôi thực hiện một thang độ sáng trên ảnh (sử dụng ct_luminance) và áp dụng bộ lọc Prewitt ( prewitt, xem wikipedia ) để phát hiện cạnh tốt:

abc abc

Sau đó là công việc thực tế: chúng ta chia hình ảnh thành 4 và đo phương sai trong mỗi góc phần tư trong hình ảnh được lọc. Phương sai của chúng tôi được tính theo kích thước của phân khu (trong bước đầu tiên này là bằng nhau), do đó, các vùng "sắc nét" có phương sai cao sẽ không được chia nhỏ hơn và nhỏ hơn và nhỏ hơn. Sau đó, chúng tôi sử dụng phương sai trọng số để nhắm mục tiêu các phân mục chi tiết hơn và lặp lại chia từng phần giàu chi tiết thành 4 phần bổ sung, cho đến khi chúng tôi đạt được số lượng trang web mục tiêu của chúng tôi (mỗi phân mục chứa chính xác một trang web). Vì chúng tôi thêm 3 trang web mỗi lần lặp lại, chúng tôi kết thúc với n - 2 <= N <= ncác trang web.

Tôi đã tạo một .webm của quy trình phân chia cho hình ảnh này, cái mà tôi không thể nhúng, nhưng nó ở đây ; màu sắc trong mỗi tiểu mục được xác định bởi phương sai trọng số. (Tôi đã tạo một loại video tương tự cho yoshi nền trắng, để so sánh, với bảng màu đảo ngược để nó chuyển sang màu trắng thay vì màu đen; nó ở đây .) Sản phẩm cuối cùng của phân mục trông như thế này:

abc

Khi chúng tôi có danh sách các phân khu, chúng tôi sẽ đi qua từng phân khu. Vị trí trang web cuối cùng là vị trí tối thiểu của hình ảnh Prewitt, nghĩa là pixel "sắc nét" nhất và màu của phần đó là màu của pixel đó; đây là hình ảnh gốc, với các trang web được đánh dấu:

abc

Sau đó, chúng tôi sử dụng tích hợp triangulateđể tính toán tam giác Delaunay của các trang web và tích hợp voronoiđể xác định các đỉnh của mỗi đa giác Voronoi, trước khi vẽ mỗi đa giác vào bộ đệm hình ảnh theo màu tương ứng. Cuối cùng, chúng tôi lưu một ảnh chụp nhanh của bộ đệm hình ảnh.

abc

Mật mã:

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

Gọi này qua voro_map, n, image, output_filename. Tôi cũng bao gồm một wrapperquy trình, trải qua từng hình ảnh thử nghiệm và chạy với 100, 500 và 1000 trang web.

Đầu ra được thu thập ở đây và đây là một số hình thu nhỏ:

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
Tôi thực sự thích thực tế là giải pháp này đặt nhiều điểm hơn vào các lĩnh vực phức tạp hơn, đó là điều tôi nghĩ là có ý định và làm cho nó khác biệt với các giải pháp khác vào thời điểm này.
alexander-brett 17/05/2015

vâng, ý tưởng về các điểm được phân nhóm chi tiết là điều khiến tôi có được sự tinh chỉnh thích nghi
sirpercival

3
Giải thích rất gọn gàng, và hình ảnh là ấn tượng! Tôi có một câu hỏi - Có vẻ như bạn nhận được nhiều hình ảnh khác nhau khi Yoshi ở trên nền trắng, nơi chúng ta có một vài hình dạng kỳ lạ. Điều gì có thể gây ra điều đó?
BrainSteel 17/05/2015

2
@BrianSteel Tôi tưởng tượng các phác thảo được chọn là các khu vực có phương sai cao và tập trung vào không cần thiết, và sau đó các khu vực thực sự chi tiết cao khác có ít điểm được chỉ định vì điều đó.
doppelgreener 17/05/2015

@BrainSteel Tôi nghĩ rằng doppel là đúng - có một lợi thế mạnh mẽ giữa viền đen và nền trắng, yêu cầu rất nhiều chi tiết trong thuật toán. Tôi không chắc liệu đây có phải là thứ tôi có thể (hoặc quan trọng hơn là nên ) sửa chữa không ...
sirpercival 17/05/2015

47

Python 3 + PIL + SciPy, Fuzzy k-nghĩa

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)

Thuật toán

Ý tưởng cốt lõi là k-nghĩa là phân cụm một cách tự nhiên hình ảnh vào các ô Voronoi, vì các điểm được gắn với trọng tâm gần nhất. Tuy nhiên, bằng cách nào đó chúng ta cần thêm các màu như một ràng buộc.

Đầu tiên chúng ta chuyển đổi từng pixel thành không gian màu Lab , để thao tác màu tốt hơn.

Sau đó, chúng tôi thực hiện một loại "phát hiện cạnh của người nghèo". Đối với mỗi pixel, chúng tôi xem xét các lân cận trực giao và đường chéo của nó và tính toán sự khác biệt trung bình bình phương về màu sắc. Sau đó, chúng tôi sắp xếp tất cả các pixel theo sự khác biệt này, với các pixel tương tự như các hàng xóm của chúng ở phía trước danh sách và các pixel giống với các hàng xóm của chúng ở phía sau (nghĩa là nhiều khả năng là một điểm cạnh). Đây là một ví dụ cho hành tinh, nơi pixel càng sáng, nó càng khác biệt so với các nước láng giềng:

nhập mô tả hình ảnh ở đây

(Có một mẫu giống như lưới rõ ràng trên đầu ra được hiển thị ở trên. Theo @randomra, điều này có thể là do mã hóa JPG bị mất hoặc imgur nén hình ảnh.)

Tiếp theo, chúng tôi sử dụng thứ tự pixel này để lấy mẫu một số lượng lớn các điểm sẽ được phân cụm. Chúng tôi sử dụng phân phối theo cấp số nhân, ưu tiên cho các điểm giống cạnh và "thú vị" hơn.

nhập mô tả hình ảnh ở đây

Để phân cụm, trước tiên chúng tôi chọn Ncentroid, được chọn ngẫu nhiên bằng cách sử dụng phân phối theo cấp số nhân như trên. Lặp lại ban đầu được thực hiện và đối với mỗi cụm kết quả, chúng ta gán một màu trung bình và ngưỡng phương sai màu. Sau đó, đối với một số lần lặp lại, chúng tôi:

  • Xây dựng tam giác Delaunay của centroid, để chúng ta có thể dễ dàng truy vấn hàng xóm đến centroid.
  • Sử dụng hình tam giác để loại bỏ các khối u có màu gần với hầu hết (> 4/5) của hàng xóm của chúng và hàng xóm của neighbour cộng lại. Bất kỳ điểm mẫu liên quan nào cũng được loại bỏ, và các điểm thay thế và điểm mẫu mới được thêm vào. Bước này cố gắng buộc thuật toán đặt nhiều cụm trong đó cần chi tiết.
  • Xây dựng một cây kd cho các centroid mới, để chúng ta có thể dễ dàng truy vấn các centroid gần nhất với bất kỳ điểm mẫu nào.
  • Sử dụng cây để gán từng điểm mẫu cho một trong 6 tâm gần nhất (6 được chọn theo kinh nghiệm). Một centroid sẽ chỉ chấp nhận một điểm mẫu nếu màu của điểm nằm trong ngưỡng phương sai màu của centroid. Chúng tôi cố gắng gán từng điểm mẫu cho centroid chấp nhận đầu tiên, nhưng nếu điều đó là không thể thì chúng tôi chỉ cần gán nó cho centroid gần nhất. "Độ mờ" của thuật toán xuất phát từ bước này, vì có thể các cụm trùng nhau.
  • Tính toán lại các trọng tâm.

nhập mô tả hình ảnh ở đây

(Bấm vào để xem kích thước đầy đủ)

Cuối cùng, chúng tôi lấy mẫu một số lượng lớn điểm, lần này bằng cách sử dụng phân phối đồng đều. Sử dụng một cây kd khác, chúng ta gán mỗi điểm cho tâm gần nhất của nó, tạo thành các cụm. Sau đó, chúng tôi ước tính màu trung vị của mỗi cụm bằng thuật toán leo đồi, đưa ra màu ô cuối cùng của chúng tôi (ý tưởng cho bước này nhờ vào @PhiNotPi và @ MartinBüttner).

nhập mô tả hình ảnh ở đây

Ghi chú

Ngoài việc xuất một tệp văn bản cho đoạn trích ( OUTFILE), nếu DEBUGđược đặt thành Truechương trình cũng sẽ xuất và ghi đè lên các hình ảnh ở trên. Thuật toán mất một vài phút cho mỗi hình ảnh, vì vậy đây là một cách tốt để kiểm tra tiến trình mà không thêm nhiều thời gian vào thời gian chạy.

Đầu ra mẫu

N = 32:

nhập mô tả hình ảnh ở đây

N = 100:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

N = 1000:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

N = 3000:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây


1
Tôi thực sự thích Yoshis màu trắng của bạn bật ra như thế nào.
Tối đa

26

Toán học, tế bào ngẫu nhiên

Đây là giải pháp cơ bản, vì vậy bạn có thể biết được mức tối thiểu tôi yêu cầu từ bạn. Đặt tên tệp (cục bộ hoặc dưới dạng URL) trong fileN trong n, đoạn mã sau chỉ đơn giản chọn ra N pixel ngẫu nhiên và sử dụng các màu được tìm thấy tại các pixel đó. Điều này thực sự ngây thơ và không hoạt động tốt, nhưng tôi muốn các bạn đánh bại điều này sau tất cả. :)

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

Dưới đây là tất cả các hình ảnh thử nghiệm cho N = 100 (tất cả các hình ảnh liên kết đến các phiên bản lớn hơn):

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Như bạn có thể thấy, những thứ này về cơ bản là vô dụng. Mặc dù chúng có thể có một số giá trị nghệ thuật, theo một cách biểu hiện, những hình ảnh ban đầu hầu như không thể nhận ra.

Đối với N = 500 , tình hình được cải thiện một chút, nhưng vẫn còn những đồ tạo tác rất kỳ quặc, hình ảnh trông bị xóa nhòa và rất nhiều ô bị lãng phí trên các vùng không có chi tiết:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Lượt của bạn!


Tôi không phải là một lập trình viên giỏi nhưng chúa ơi những hình ảnh này trông rất đẹp. Ý tưởng tuyệt vời
Faraz Masroor

Bất kỳ lý do cho Dimensions@ImageDatavà không ImageDimensions? Bạn có thể tránh ImageDatahoàn toàn chậm bằng cách sử dụng PixelValue.
2012rcampion 17/05/2015

@ 2012rcampion Không có lý do, tôi chỉ không biết một trong hai chức năng tồn tại. Tôi có thể sửa nó sau và cũng thay đổi hình ảnh ví dụ thành các giá trị N được đề xuất.
Martin Ender

23

Toán học

Chúng ta đều biết Martin yêu Mathicala vì vậy hãy để tôi thử nó với Mathematica.

Thuật toán của tôi sử dụng các điểm ngẫu nhiên từ các cạnh hình ảnh để tạo ra một sơ đồ voronoi ban đầu. Sơ đồ sau đó được hoàn thiện bằng cách điều chỉnh lặp lại của lưới với bộ lọc trung bình đơn giản. Điều này mang lại hình ảnh với mật độ tế bào cao gần các vùng tương phản cao và các tế bào đẹp mắt mà không có góc điên.

Các hình ảnh sau đây cho thấy một ví dụ về quá trình đang hoạt động. Sự thú vị có phần bị làm hỏng bởi Antialiasing Antialiasing, nhưng chúng ta có đồ họa vector, đó phải là thứ đáng giá.

Thuật toán này, không có lấy mẫu ngẫu nhiên, có thể được tìm thấy trong VoronoiMeshtài liệu ở đây .

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Hình ảnh thử nghiệm (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]}]]
   ];

Tốt công việc cho một bài viết đầu tiên! :) Bạn có thể muốn thử hình ảnh thử nghiệm Voronoi với 32 ô (đó là số lượng ô trong chính hình ảnh).
Martin Ender

Cảm ơn! Tôi đoán thuật toán của tôi sẽ thực hiện khủng khiếp trong ví dụ này. Các hạt giống sẽ được khởi tạo ở các cạnh của tế bào và sự đệ quy sẽ không làm cho nó tốt hơn nhiều;)
paw

Mặc dù tốc độ chậm hơn để hội tụ vào hình ảnh gốc, tôi thấy rằng thuật toán của bạn mang lại một kết quả rất nghệ thuật! (giống như một phiên bản cải tiến của Georges Seurat hoạt động). Bạn đã làm rất tốt!
neizod

Bạn cũng có thể có được màu sắc đa giác được nội suy bằng thủy tinh bằng cách thay đổi các dòng cuối cùng của bạn thànhGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
Biểu đồ

13

Python + SciPy + emcee

Thuật toán tôi đã sử dụng là như sau:

  1. Thay đổi kích thước hình ảnh thành kích thước nhỏ (~ 150 pixel)
  2. Tạo một hình ảnh không bị che khuất của các giá trị kênh tối đa (điều này giúp không thu được các vùng trắng quá mạnh).
  3. Lấy giá trị tuyệt đối.
  4. Chọn các điểm ngẫu nhiên với xác suất tỷ lệ thuận với hình ảnh này. Điều này chọn điểm một trong hai bên của sự không liên tục.
  5. Tinh chỉnh các điểm đã chọn để hạ thấp hàm chi phí. Hàm này là mức tối đa của tổng độ lệch bình phương trong các kênh (một lần nữa giúp thiên vị cho các màu đơn sắc và không chỉ có màu trắng đặc). Tôi đã sử dụng sai Markov Chain Monte Carlo với mô-đun emcee (rất khuyến khích) làm trình tối ưu hóa. Quy trình bảo lãnh khi không có cải tiến mới được tìm thấy sau các lần lặp chuỗi N.

Các thuật toán dường như hoạt động rất tốt. Thật không may, nó chỉ có thể chạy hợp lý trên hình ảnh nhỏ. Tôi không có thời gian để lấy điểm Voronoi và áp dụng chúng cho những hình ảnh lớn hơn. Họ có thể được tinh chế tại thời điểm này. Tôi cũng có thể chạy MCMC lâu hơn để có cực tiểu tốt hơn. Điểm yếu của thuật toán là nó khá đắt. Tôi đã không có thời gian để tăng hơn 1000 điểm và một vài trong số 1000 hình ảnh thực sự vẫn đang được tinh chỉnh.

(nhấp chuột phải và xem hình ảnh để có phiên bản lớn hơn)

Hình thu nhỏ cho 100, 300 và 1000 điểm

Liên kết đến các phiên bản lớn hơn là http://imgur.com/a/2IXDT#9 (100 điểm), http://imgur.com/a/bBQ7q (300 điểm) và http://imgur.com/a/rr8wJ (1000 điểm)

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

Hình ảnh không che mặt trông như sau. Điểm ngẫu nhiên được chọn từ hình ảnh nếu một số ngẫu nhiên nhỏ hơn giá trị của hình ảnh (được định mức là 1):

Hình ảnh Saturn không che

Tôi sẽ đăng những hình ảnh lớn hơn và các điểm Voronoi nếu tôi có nhiều thời gian hơn.

Chỉnh sửa: Nếu bạn tăng số lượng người đi bộ lên 100 * npts, hãy thay đổi hàm chi phí thành một số bình phương của độ lệch trong tất cả các kênh và chờ trong một thời gian dài (tăng số lần lặp để thoát ra khỏi vòng lặp thành 200), có thể tạo ra một số hình ảnh tốt chỉ với 100 điểm:

Hình 11, 100 điểm Hình 2, 100 điểm Hình 4, 100 điểm Hình 10, 100 điểm


3

Sử dụng năng lượng hình ảnh như một bản đồ trọng lượng điểm

Theo cách tiếp cận của tôi đối với thử thách này, tôi muốn có một cách để ánh xạ "mức độ phù hợp" của một khu vực hình ảnh cụ thể đến xác suất rằng một điểm cụ thể sẽ được chọn là một nhân mã Voronoi. Tuy nhiên, tôi vẫn muốn duy trì cảm giác nghệ thuật của việc ghép Voronoi bằng cách chọn ngẫu nhiên các điểm hình ảnh. Ngoài ra, tôi muốn thao tác trên các hình ảnh lớn, vì vậy tôi không mất gì trong quá trình lấy mẫu. Thuật toán của tôi đại khái như thế này:

  1. Đối với mỗi hình ảnh, tạo một bản đồ sắc nét. Bản đồ độ sắc nét được xác định bởi năng lượng hình ảnh chuẩn hóa (hoặc bình phương của tín hiệu tần số cao của hình ảnh). Một ví dụ trông như thế này:

Bản đồ sắc nét

  1. Tạo một số điểm từ hình ảnh, lấy 70 phần trăm từ các điểm trong bản đồ độ sắc nét và 30 phần trăm từ tất cả các điểm khác. Điều này có nghĩa là các điểm được lấy mẫu dày đặc hơn từ các phần có độ chi tiết cao của hình ảnh.
  2. Màu sắc!

Các kết quả

N = 100, 500, 1000, 3000

Hình 1, N = 100 Hình 1, N = 500 Hình 1, N = 1000 Hình 1, N = 3000

Hình 2, N = 100 Hình 2, N = 500 Hình 2, N = 1000 Hình 2, N = 3000

Hình 3, N = 100 Hình 3, N = 500 Hình 3, N = 1000 Hình 3, N = 3000

Hình 4, N = 100 Hình 4, N = 500 Hình 4, N = 1000 Hình 4, N = 3000

Hình 5, N = 100 Hình 5, N = 500 Hình 5, N = 1000 Hình 5, N = 3000

Hình 6, N = 100 Hình 6, N = 500 Hình 6, N = 1000 Hình 6, N = 3000

Hình 7, N = 100 Hình 7, N = 500 Hình 7, N = 1000 Hình 7, N = 3000

Hình 8, N = 100 Hình 8, N = 500 Hình 8, N = 1000 Hình 8, N = 3000

Hình 9, N = 100 Hình 9, N = 500 Hình 9, N = 1000 Hình 9, N = 3000

Hình 10, N = 100 Hình 10, N = 500 Hình 10, N = 1000 Hình 10, N = 3000

Hình 11, N = 100 Hình 11, N = 500 Hình 11, N = 1000 Hình 11, N = 3000

Hình 12, N = 100 Hình 12, N = 500 Hình 12, N = 1000 Hình 12, N = 3000

Hình 13, N = 100 Hình 13, N = 500 Hình 13, N = 1000 Hình 13, N = 3000

Hình 14, N = 100 Hình 14, N = 500 Hình 14, N = 1000 Hình 14, N = 3000


14
Bạn có phiền a) bao gồm mã nguồn được sử dụng để tạo cái này và b) liên kết từng hình thu nhỏ với hình ảnh kích thước đầy đủ không?
Martin Ender
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.