ボロノイマップとして画像を描く


170

私のチャレンジのアイデアを正しい方向に向けてくれたCalvin's Hobbiesに感謝します。

プレーン内のポイントのセットを考えてみましょう。これをsitesと呼び、各サイトに色を関連付けます。これで、各ポイントを最も近いサイトの色で着色することで、平面全体をペイントできます。これは、ボロノイマップ(またはボロノイ図)と呼ばれます。原則として、ボロノイマップは任意の距離メトリックに対して定義できますが、通常のユークリッド距離を使用しますr = √(x² + y²)注:これらのいずれかを計算してレンダリングする方法を知っていなくても、このチャレンジに参加できます。)

100サイトの例を次に示します。

ここに画像の説明を入力してください

セルを見ると、そのセル内のすべてのポイントは、他のサイトよりも対応するサイトに近くなっています。

あなたの仕事は、与えられた画像をそのようなボロノイマップで近似することです。便利なラスターグラフィックス形式の画像と整数Nが与えられます。次に、最大N個のサイトと各サイトの色を作成し、これらのサイトに基づいたボロノイマップができるだけ入力画像に似るようにします。

このチャレンジの下部にあるStack Snippetを使用して、出力からボロノイマップをレンダリングするか、必要に応じて自分でレンダリングすることができます。

あなたは可能(必要であれば)サイトの集合からボロノイマップを計算するために、内蔵またはサードパーティの機能を使用しています。

これは人気のあるコンテストなので、正味の投票数が最も多い回答が勝ちます。投票者は次の方法で回答を判断することが推奨されます

  • 元の画像とその色がどの程度近似されているか。
  • アルゴリズムがさまざまな種類の画像でどれだけうまく機能するか。
  • アルゴリズムが小さいNに対してどれだけうまく機能するか。
  • アルゴリズムが、より詳細を必要とする画像の領域内のポイントを適応的にクラスタリングするかどうか。

テスト画像

アルゴリズムをテストするためのいくつかの画像を次に示します(通常の容疑者の一部、新しいもの)。大きなバージョンの画像をクリックします。

グレートウェーブ ハリネズミ ビーチ コーネル 土星 ヒグマ ヨッシー マンドリル カニ星雲 ジオビットの子供 滝 悲鳴

最初の列のビーチはオリビア・ベルによって描かれ、彼女の許可を得て含まれています。

さらにチャレンジしたい場合は、白い背景でヨッシーを試し、腹のラインを正しくします。

これらのテスト画像すべて、このimgurギャラリーで見つけることができます。すべてのzipファイルとしてダウンロードできます。アルバムには、別のテストとしてランダムなボロノイ図も含まれています。参考までに、生成したデータを以下に示します。

さまざまな異なる画像とNの例図(100、300、1000、3000など)を含めてください(同様に、対応するセル仕様の一部へのペーストビン)。セル間の黒いエッジを適切に使用または省略できます(これは、他の画像よりも一部の画像の方が見やすい場合があります)。ただし、サイトを含めないでください(もちろん、サイトの配置がどのように機能するかを説明したい場合は別の例を除きます)。

多数の結果を表示する場合は、imgur.comでギャラリーを作成して、回答のサイズを適切に保つことができます。別の方法として、投稿にサムネイルを配置し、参照回答で行っように、より大きな画像へのリンクを作成します。simgur.comリンクのファイル名に追加することにより、小さなサムネイルを取得できます(例I3XrT.png-> I3XrTs.png)。また、何か良いものが見つかった場合は、他のテストイメージを自由に使用してください。

レンダラー

出力を次のスタックスニペットに貼り付けて、結果をレンダリングします。正確なリスト形式は、各セルが順番x y r g bに5つの浮動小数点数で指定されている限り無関係です。ここでxおよびyはセルのサイトの座標でありr g b、範囲内の赤、緑、青の色チャンネルです0 ≤ r, g, b ≤ 1

このスニペットには、セルの端の線幅、およびセルサイトを表示するかどうかを指定するオプションがあります(後者は主にデバッグ目的で使用されます)。ただし、セルの仕様が変更された場合にのみ出力が再レンダリングされることに注意してください。他のオプションを変更する場合は、セルまたは何かにスペースを追加してください。

この非常に素晴らしいJS Voronoiライブラリを書いたことに対して、Raymond Hillに多大な貢献をしています

関連する課題


5
@frogeyedpeasあなたが得た票を見ることで。;)これは人気コンテストです。必ずしもありません行うための最善の方法は。アイデアは、できる限りそれをしようとすることであり、投票はあなたが良い仕事をしたことに人々が同意するかどうかを反映します。確かに、これらにはある程度の主観性があります。私がリンクした関連する課題、またはこの課題をご覧ください。通常、さまざまなアプローチがありますが、投票システムは、より良いソリューションがトップにバブルし、勝者を決定するのに役立ちます。
マーティンエンダー

3
オリビアは、これまでに提出されたビーチの近似値を承認します。
アレックスA.

3
@AlexA。デボンは、これまでに提出された彼の顔の近似のいくつかを承認します。彼はn = 100バージョンの大ファンではありません;)
Geobits

1
@Geobits:彼は年をとるとわかる。
アレックスA.

1
これは、重心ボロノイベースの点描技術に関するページです。インスピレーションの良い情報源(関連する修士論文では、アルゴリズムの改善の可能性について良い議論をしています)。
ジョブ

回答:


112

Python + scipy + scikit-image、重み付けポアソンディスクサンプリング

私の解決策はかなり複雑です。ノイズを除去し、各ポイントがどのように「興味深い」かをマッピングするために画像の前処理を行います(ローカルエントロピーとエッジ検出の組み合わせを使用):

次に、ツイストポアソンディスクサンプリングを使用してサンプリングポイントを選択します。円の距離は、先ほど決定した重みによって決まります。

次に、サンプリングポイントを取得したら、画像をボロノイセグメントに分割し、各セグメント内のカラー値のL * a * b *平均を各セグメントに割り当てます。

ヒューリスティックがたくさんあります。また、サンプルポイントの数がに近いことを確認するために少し計算する必要がありNます。わずかNオーバーシュートし、ヒューリスティックでいくつかのポイントをドロップすることで正確に取得します。

ランタイムに関しては、このフィルターは安価ではありませんが、以下の画像を作成するのに5秒以上かかりませんでした。

難しい話は抜きにして:

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

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


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


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

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

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

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

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

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

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

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

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

        if len(distances) == 0:
            return True

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

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

        return False

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

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

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

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

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

    return sample_points


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

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

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

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

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

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

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

    return color_samples


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

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

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

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


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

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

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

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

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

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

        print("Done!")

画像

それぞれ100、300、1000、3000 Nです。

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
私はこれが好き; スモークグラスに少し似ています。
BobTheAwesome

3
私はこれを少しいじりましたが、denoise_bilatteralをdenoise_tv_bregmanに置き換えると、特に低三角形の画像でより良い結果が得られます。ノイズ除去でより均一なパッ​​チを生成するため、役立ちます。
LKlevin

@LKlevinあなたはどの体重を使用しましたか?
orlp

重みとして1.0を使用しました。
LKlevin

65

C ++

私のアプローチは非常に遅いですが、それがもたらす結果の品質、特にエッジの保存に関して非常に満足しています。たとえば、YoshiCornell Boxにはそれぞれ1000サイトしかありません。

それをカチカチ音にする2つの主要な部分があります。最初に、evaluate()関数で具体化された候補サイトの場所のセットを取得し、それらに最適な色を設定し、レンダリングされたボロノイテッセレーション対ターゲットイメージのPSNRのスコアを返します。各サイトの色は、サイト周辺のセルで覆われているターゲット画像のピクセルを平均することにより決定されます。Welfordのアルゴリズムを使用して、分散、MSE、およびPSNRの関係を活用することにより、画像上の単一パスを使用して、各セルの最適な色と結果のPSNRの両方を計算します。これにより、色を特に考慮せずにサイトの場所の最適なセットを見つけるという問題を軽減できます。

次に、で具体化された2番目の部分は、main()このセットを見つけようとします。まず、一連のポイントをランダムに選択することから始めます。次に、各ステップで1つのポイントを削除し(ラウンドロビンに移行)、ランダムな候補ポイントのセットをテストして置き換えます。束のPSNRが最も高いものが受け入れられ、保持されます。事実上、これによりサイトは新しい場所にジャンプし、通常は画像を少しずつ改善します。アルゴリズムは、元の場所を候補として意図的に保持しないことに注意してください。時々、これはジャンプが全体的な画質を低下させることを意味します。これを可能にすることは、極大値で立ち往生することを避けるのに役立ちます。また、停止基準も提供します。プログラムは、これまでに見つかった最良のサイトセットを改善することなく、特定の数のステップを実行した後に終了します。

この実装はかなり基本的なものであり、特にサイトの数が増えると、数時間のCPUコア時間がかかります。すべての候補について完全なボロノイマップを再計算し、ブルートフォースは各ピクセルのすべてのサイトまでの距離をテストします。各操作には一度に1つのポイントを削除し、別のポイントを追加する必要があるため、各ステップでの画像の実際の変更はかなりローカルになります。ボロノイ図を効率的にインクリメンタルに更新するためのアルゴリズムがあり、このアルゴリズムが非常に高速化されると信じています。ただし、このコンテストでは、物事をシンプルかつ総当たり的にすることを選択しました。

コード

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

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

using namespace std;

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

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

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

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

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

ランニング

このプログラムは自己完結型であり、標準ライブラリを超える外部依存関係はありませんが、イメージはバイナリPPM形式である必要があります。私が使用してImageMagickのを GIMPとかなりの数の他のプログラムがあまりにもそれを行うことができますが、PPMに画像を変換します。

コンパイルするには、プログラムをとして保存してvoronoi.cppから実行します:

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

私はこれを試したことはありませんが、おそらく最新バージョンのVisual Studioを搭載したWindowsで動作することを期待しています。C ++ 11以上でコンパイルし、OpenMPが有効になっていることを確認する必要があります。OpenMPは必ずしも必要ではありませんが、実行時間をより許容できるものにする上で非常に役立ちます。

実行するには、次のようにします。

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

後のファイルは、サイトのデータで更新されます。最初の行には画像の幅と高さがあり、その後に問題の説明のJavascriptレンダラーへのコピーと貼り付けに適したx、y、r、g、b値の行が続きます。

プログラムの上部にある3つの定数を使用して、速度と品質を調整できます。このdecimation係数は、色とPSNRのサイトセットを評価するときにターゲットイメージを粗くします。値が大きいほど、プログラムの実行は速くなります。1に設定すると、フル解像度の画像が使用されます。candidates一定のコントロールはどのように多くの候補者の各段階でテストします。高いほど、ジャンプするのに適した場所を見つける可能性が高くなりますが、プログラムが遅くなります。最後に、terminationプログラムが終了する前に出力を改善せずに何ステップできるかです。値を大きくすると、より良い結果が得られる場合がありますが、少し時間がかかります。

画像

N = 100、300、1000、および3000:


1
これはIMOで勝ったはずです。
orlp

1
@orlp-ありがとう!公平を期すために、あなたはより早くあなたのものを投稿し、それははるかに迅速に実行されます。スピードが重要です!
ブジュム

1
まあ、私は本当にボロノイマップの答えではありませ :)それは本当に良いサンプリングアルゴリズムですが、サンプルポイントをボロノイサイトに変えることは明らかに最適ではありません。
orlp

55

IDL、適応調整

この方法は、天体シミュレーションからのアダプティブメッシュリファインメントと、サブディビジョンサーフェスに触発されています。これはIDLが誇りにしている種類のタスクです。これは、私が使用できた多数の組み込み関数によってわかります。:D

私は、黒背景yoshiテスト画像の中間体をいくつか出力しましたn = 1000

まず、画像に対して輝度グレースケールを実行し(を使用ct_luminance)、良好なエッジ検出のためにPrewittフィルター(prewittウィキペディアを参照)を適用します。

abc abc

次に、実際のうんざりした作業が行われます。画像を4つに細分割し、フィルター処理された画像の各象限の分散を測定します。分散はサブディビジョンのサイズ(この最初のステップでは等しい)で重み付けされるため、分散の高い「エッジのある」領域はますます細かく分割されません。次に、重み付き分散を使用して、より詳細なサブディビジョンをターゲットに設定し、サイトのターゲット数に達するまで、各ディテールが豊富なセクションをさらに4つに分割します(各サブディビジョンには1つのサイトが含まれます)。繰り返すたびに3つのサイトを追加するため、最終的にn - 2 <= N <= nサイトになります。

この画像の再分割プロセスの.webmを作成しましたが、埋め込むことはできませんが、ここにあります。各サブセクションの色は、加重分散によって決定されます。(比較のために、白の背景yoshiに対して同じ種類のビデオを作成しました。カラーテーブルを逆にして、黒ではなく白に向かっていますここにあります。)細分化の最終製品は次のようになります。

abc

サブディビジョンのリストを作成したら、各サブディビジョンを調べます。最終的なサイトの場所は、Prewitt画像の最小の位置、つまり最も「エッジのない」ピクセルであり、セクションの色はそのピクセルの色です。ここに元の画像と、マークされたサイトがあります:

abc

次に、ビルトインを使用しtriangulateてサイトのドロネー三角形分割を計算し、ビルトインを使用してvoronoi各ボロノイポリゴンの頂点を定義してから、各ポリゴンをそれぞれの色の画像バッファーに描画します。最後に、画像バッファのスナップショットを保存します。

abc

コード:

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

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

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

経由でこれを呼び出しvoro_map, n, image, output_filenameます。wrapper同様に、各テストイメージを通過し、100、500、および1000のサイトで実行する手順を含めました。

ここで収集された出力と、いくつかのサムネイルを次に示します。

n = 100

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
私はこのソリューションがより複雑な領域により多くのポイントを置くという事実が本当に好きです。それは私が意図していることだと思い、この時点で他のものから区別します。
アレクサンダーブレット

ええ、詳細にグループ化されたポイントのアイデアは、私を適応的洗練に導いたものです
サーパーシバル

3
非常にきちんとした説明、そして画像は印象的です!質問があります-ヨッシーが白い背景にいるとき、あなたは非常に異なる画像を取得するようです。何が原因ですか?
BrainSteel

2
@BrianSteelアウトラインが高分散領域としてピックアップされ、不必要に焦点を当てると、他の真に高詳細な領域に割り当てられるポイントが少なくなると思います。
doppelgreener

@BrainSteel私はドッペルが正しいと思います-黒い境界線と白い背景の間に強いエッジがあり、アルゴリズムの多くの詳細を求めます。これが私が解決できる(または、より重要なの)かどうかわからない
...-サーパーシバル

47

Python 3 + PIL + SciPy、ファジーk-means

from collections import defaultdict
import itertools
import random
import time

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

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

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

"""
Color conversion functions
"""

start_time = time.time()

# http://www.easyrgb.com/?X=MATH
def rgb2xyz(rgb):
  r, g, b = rgb
  r /= 255
  g /= 255
  b /= 255

  r = ((r + 0.055)/1.055)**2.4 if r > 0.04045 else r/12.92
  g = ((g + 0.055)/1.055)**2.4 if g > 0.04045 else g/12.92
  b = ((b + 0.055)/1.055)**2.4 if b > 0.04045 else b/12.92

  r *= 100
  g *= 100
  b *= 100

  x = r*0.4124 + g*0.3576 + b*0.1805
  y = r*0.2126 + g*0.7152 + b*0.0722
  z = r*0.0193 + g*0.1192 + b*0.9505

  return (x, y, z)

def xyz2lab(xyz):
  x, y, z = xyz
  x /= 95.047
  y /= 100
  z /= 108.883

  x = x**(1/3) if x > 0.008856 else 7.787*x + 16/116
  y = y**(1/3) if y > 0.008856 else 7.787*y + 16/116
  z = z**(1/3) if z > 0.008856 else 7.787*z + 16/116

  L = 116*y - 16
  a = 500*(x - y)
  b = 200*(y - z)

  return (L, a, b)

def rgb2lab(rgb):
  return xyz2lab(rgb2xyz(rgb))

def lab2xyz(lab):
  L, a, b = lab
  y = (L + 16)/116
  x = a/500 + y
  z = y - b/200

  y = y**3 if y**3 > 0.008856 else (y - 16/116)/7.787
  x = x**3 if x**3 > 0.008856 else (x - 16/116)/7.787
  z = z**3 if z**3 > 0.008856 else (z - 16/116)/7.787

  x *= 95.047
  y *= 100
  z *= 108.883

  return (x, y, z)

def xyz2rgb(xyz):
  x, y, z = xyz
  x /= 100
  y /= 100
  z /= 100

  r = x* 3.2406 + y*-1.5372 + z*-0.4986
  g = x*-0.9689 + y* 1.8758 + z* 0.0415
  b = x* 0.0557 + y*-0.2040 + z* 1.0570

  r = 1.055 * (r**(1/2.4)) - 0.055 if r > 0.0031308 else 12.92*r
  g = 1.055 * (g**(1/2.4)) - 0.055 if g > 0.0031308 else 12.92*g
  b = 1.055 * (b**(1/2.4)) - 0.055 if b > 0.0031308 else 12.92*b

  r *= 255
  g *= 255
  b *= 255

  return (r, g, b)

def lab2rgb(lab):
  return xyz2rgb(lab2xyz(lab))

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

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

pixlab_map = {}

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

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

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

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


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

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

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

    return results

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

features = []

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

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

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

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

    test_im.save(FEATURE_FILE)

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

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

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

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

sample_points = set()

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

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

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

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

    test_im.save(SAMPLE_FILE)

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

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

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

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

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

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

    start_choices.append(mean_col)

    best_dist = None
    col = None

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

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

    # Approximate median by hill climbing
    last = None

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

        best_dist = None
        best_col = None

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

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

        col = best_col

    return col

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

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

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

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

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

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

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

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

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

        test_im.save(SAMPLE_FILE)

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

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

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

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

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

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

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

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

            like_neighbours = True

            nns = set() # neighbours + neighbours of neighbours

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

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

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

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

                clusters[num] = set()

    new_centroids = []

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

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

    clusters = defaultdict(set)

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

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

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

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

"""
Step 4: Output
"""

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

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

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

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

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

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

アルゴリズム

核となる考え方は、ポイントが最も近い重心に結び付けられているため、k-meansクラスタリングは画像をボロノイセルに自然に分割するというものです。ただし、何らかの形で制約として色を追加する必要があります。

最初に、各ピクセルをLabカラースペースに変換して、カラー操作を改善します。

次に、一種の「貧しい人のエッジ検出」を行います。各ピクセルについて、その直交および対角線上の近傍を見て、色の平均二乗差を計算します。次に、この差ですべてのピクセルをソートします。リストの前にあるピクセルと最も類似しているピクセル、および後ろにあるピクセルと最も類似していないピクセル(エッジポイントである可能性が高い)を並べます。以下は惑星の例です。ピクセルが明るいほど、隣のピクセルとは異なります。

ここに画像の説明を入力してください

(上記のレンダリング出力には明確なグリッド状のパターンがあります。@ randomraによると、これはおそらく不可逆JPGエンコードまたは画像の圧縮によるものです。)

次に、このピクセル順序付けを使用して、クラスター化する多数のポイントをサンプリングします。指数分布を使用して、よりエッジに似た「興味深い」ポイントを優先します。

ここに画像の説明を入力してください

クラスタリングでは、最初にN、上記と同じ指数分布を使用してランダムに選択された重心を選択します。最初の反復が実行され、結果のクラスターごとに平均色と色分散のしきい値を割り当てます。その後、多数の反復について、次のことを行います。

  • 重心のDelaunay三角形分割を作成して、重心の近傍を簡単に照会できるようにします。
  • 三角形分割を使用して、隣人のほとんど(> 4/5)と隣人の隣人を合わせた色に近い重心を削除します。関連するサンプルポイントもすべて削除され、新しい置換重心とサンプルポイントが追加されます。このステップでは、詳細が必要な場所にさらにクラスターを配置するようにアルゴリズムを強制しようとします。
  • 任意のサンプルポイントに最も近い重心を簡単に照会できるように、新しい重心のkdツリーを構築します。
  • ツリーを使用して、各サンプルポイントを6つの最も近い重心(経験的に選択された6つ)のいずれかに割り当てます。重心は、ポイントの色が重心の色分散しきい値内にある場合にのみサンプルポイントを受け入れます。各サンプルポイントを最初に受け入れる重心に割り当てようとしますが、それが不可能な場合は、単純に最も近い重心に割り当てます。クラスターのオーバーラップが発生する可能性があるため、アルゴリズムの「あいまいさ」はこのステップから生じます。
  • 重心を再計算します。

ここに画像の説明を入力してください

(フルサイズをクリックします)

最後に、今回は均一な分布を使用して、多数のポイントをサンプリングします。別のkdツリーを使用して、各ポイントを最も近い重心に割り当て、クラスターを形成します。次に、山登りアルゴリズムを使用して各クラスターの中央値の色を近似し、最終的なセルの色を指定します(@PhiNotPiと@MartinBüttnerに感謝します)。

ここに画像の説明を入力してください

ノート

スニペット(OUTFILE)のテキストファイルを出力することに加えて、if DEBUGTrueプログラムに設定されている場合、上記の画像も出力および上書きされます。このアルゴリズムは各画像に数分かかるため、進行状況を確認するのに適した方法であり、実行時間にそれほど多くを追加することはありません。

サンプル出力

N = 32:

ここに画像の説明を入力してください

N = 100:

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください

N = 1000:

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください

N = 3000:

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください


1
私はあなたの白いヨッシーがどれほどうまくできたかが本当に好きです。
マックス

26

Mathematica、ランダムセル

これがベースラインソリューションであるため、あなたが私に求めている最低限のアイデアが得られます。ファイル名(ローカルまたはURL)in fileおよびN inを指定するnと、次のコードはN個のランダムなピクセルを選択し、それらのピクセルで見つかった色を使用します。これは本当に素朴で信じられないほどうまく機能しませんが、結局は皆さんにこれを打ち負かしてほしいです。:)

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

以下は、N = 100のすべてのテストイメージです(すべてのイメージは、より大きなバージョンにリンクしています)。

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください

ご覧のとおり、これらは本質的には役に立ちません。表現主義的な方法で芸術的な価値があるかもしれませんが、元の画像はほとんど認識できません。

以下のためにN = 500、状況は少し改善されるが、非常に奇妙なアーティファクトは、画像が洗い流さ見て、細胞の多くは詳細なし領域に浪費され、残っています。

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください

あなたの番!


私は良いコーダーではありませんが、これらの画像は見た目が美しいです。素晴らしいアイデア!
ファラズマスロア

理由Dimensions@ImageDataImageDimensions何ですか?ImageDataを使用すると、低速を完全に回避できPixelValueます。
2012rcampion

@ 2012rcampion理由はありませんが、どちらの機能が存在するのか知りませんでした。後で修正し、サンプル画像を推奨されるN値に変更することもできます。
マーティンエンダー

23

Mathematica

MartinがMathematicaを愛していることは誰もが知っているので、Mathematicaで試してみましょう。

私のアルゴリズムは、画像のエッジからランダムな点を使用して、初期ボロノイ図を作成します。次に、単純な平均フィルターを使用してメッシュを繰り返し調整することにより、ダイアグラムを明確にします。これにより、コントラストの高い領域の近くに高い細胞密度の画像が表示され、狂った角度のない視覚的に快適な細胞が得られます。

次の画像は、実行中のプロセスの例を示しています。Mathematicaの悪いアンチエイリアシングによって楽しさが損なわれていますが、ベクターグラフィックスが得られます。

ランダムサンプリングを行わないこのアルゴリズムについては、こちらVoronoiMeshドキュメントをご覧ください

ここに画像の説明を入力してください ここに画像の説明を入力してください

テスト画像(100,300,1000,3000)

コード

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

最初の投稿は素晴らしい仕事です!:) 32個のセルを持つボロノイテスト画像を試してみてください(画像自体のセルの数)。
マーティンエンダー

ありがとう!私のアルゴリズムはこの例ではひどく動作すると思います。シードはセルの端で初期化され、再帰ではそれほど良くなりません;)
paw

元の画像に収束するのに遅い速度にもかかわらず、私はあなたのアルゴリズムが非常に芸術的な結果をもたらすと思います!(ジョルジュ・スーラ作品の改良版のような)。よくやった!
neizod

最終ラインを次のように変更することで、ガラスのような補間ポリゴンカラーを得ることができますGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
ヒストグラム

13

Python + SciPy +司会者

私が使用したアルゴリズムは次のとおりです。

  1. 画像のサイズを小さいサイズ(最大150ピクセル)に変更します。
  2. 最大チャネル値のマスクされていない画像を作成します(これにより、白い領域を強くピックアップしすぎないようにします)。
  3. 絶対値を取ります。
  4. この画像に比例する確率でランダムポイントを選択します。これにより、不連続点の両側のポイントが選択されます。
  5. 選択したポイントを調整して、コスト関数を下げます。この関数は、チャネルの偏差の2乗の合計の最大値です(ここでも、白だけでなく、無地の色にバイアスをかけるのに役立ちます)。最適化ツールとして、司会者モジュール(強く推奨)でマルコフ連鎖モンテカルロを誤用しました。Nチェーンの反復後に新しい改善が見つからない場合、手順は終了します。

アルゴリズムは非常にうまく機能しているようです。残念ながら、小さな画像でしか実行できません。ボロノイポイントを取得して大きな画像に適用する時間はありませんでした。これらはこの時点で改良することができます。より良い最小値を得るために、MCMCをより長く実行することもできました。アルゴリズムの弱点は、かなり高価なことです。1000ポイントを超える時間はありませんでしたが、1000ポイントの画像のいくつかは実際にはまだ洗練されています。

(右クリックして画像を表示すると、より大きなバージョンになります)

100、300、および1000ポイントのサムネイル

より大きなバージョンへのリンクは、http://imgur.com/a/2IXDT#9(100ポイント)、http://imgur.com/a/bBQ7q(300ポイント)およびhttp://imgur.com/a/rr8wJです。(1000ポイント)

#!/usr/bin/env python

import glob
import os

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

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

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

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

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

    dist, idx = tree.query(queryxy)

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

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

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

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

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

    # return fake log probability
    return -fit

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

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

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

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

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

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

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

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

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

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

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

            ct_nobetter = 0
            print(lastmax)

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

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

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

            process_image(infile, outroot, npts)

if __name__ == '__main__':
    main()

シャープでないマスクされた画像は次のようになります。乱数が画像の値(1にノルム)より小さい場合、画像からランダムポイントが選択されます。

マスクされていない土星画像

時間があれば、より大きな画像とボロノイポイントを投稿します。

編集:歩行者の数を100 * nptsに増やした場合、コスト関数をすべてのチャネルの偏差の2乗の一部に変更し、長時間待機します(ループから抜け出すための反復回数を増やします) 200)、わずか100ポイントでいくつかの良い画像を作成することが可能です:

画像11、100ポイント 画像2、100ポイント 画像4、100ポイント 画像10、100ポイント


3

画像エネルギーをポイントウェイトマップとして使用する

この課題に対する私のアプローチでは、特定の画像領域の「関連性」を、特定のポイントがボロノイ重心として選択される確率にマッピングする方法が必要でした。ただし、画像ポイントをランダムに選択することで、ボロノイモザイクの芸術的な雰囲気を維持したいと考えました。さらに、大きな画像を操作したいので、ダウンサンプリングプロセスで何も失われません。私のアルゴリズムはおおよそ次のようなものです。

  1. 各画像について、鮮明度マップを作成します。鮮明度マップは、正規化された画像エネルギー(または画像の高周波信号の2乗)によって定義されます。例は次のようになります。

シャープネスマップ

  1. 画像から多数のポイントを生成します。シャープネスマップのポイントから70%、他のすべてのポイントから30%を取得します。これは、画像の詳細部分からポイントがより高密度にサンプリングされることを意味します。
  2. 色!

結果

N = 100、500、1000、3000

画像1、N = 100 画像1、N = 500 画像1、N = 1000 画像1、N = 3000

画像2、N = 100 画像2、N = 500 画像2、N = 1000 画像2、N = 3000

画像3、N = 100 画像3、N = 500 画像3、N = 1000 画像3、N = 3000

画像4、N = 100 画像4、N = 500 画像4、N = 1000 画像4、N = 3000

画像5、N = 100 画像5、N = 500 画像5、N = 1000 画像5、N = 3000

画像6、N = 100 画像6、N = 500 画像6、N = 1000 画像6、N = 3000

画像7、N = 100 画像7、N = 500 画像7、N = 1000 画像7、N = 3000

画像8、N = 100 画像8、N = 500 画像8、N = 1000 画像8、N = 3000

画像9、N = 100 画像9、N = 500 画像9、N = 1000 画像9、N = 3000

画像10、N = 100 画像10、N = 500 画像10、N = 1000 画像10、N = 3000

画像11、N = 100 画像11、N = 500 画像11、N = 1000 画像11、N = 3000

画像12、N = 100 画像12、N = 500 画像12、N = 1000 画像12、N = 3000

画像13、N = 100 画像13、N = 500 画像13、N = 1000 画像13、N = 3000

画像14、N = 100 画像14、N = 500 画像14、N = 1000 画像14、N = 3000


14
a)これを生成するために使用されるソースコードを含めて、b)各サムネイルをフルサイズの画像にリンクしてください。
マーティンエンダー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.