fullseye

セグメンテーション — 使い方ガイド

この族は何をする道具箱か

このファミリは 「グレー画像 → 領域(region)」への分割 を担う演算子群です。入力はどの op も in_sort = image(2 次元 float64、値域 [0,1])、出力はほぼすべて out_sort = region (入力と同じ形の 2 次元 float64 で、値は {0, 1}二値マスク)。つまり「各画素は前景か 背景か / どの区画に属するか」を決めて、後段の領域モルフォロジや計測が扱える形に落とす道具です。

やり方の系統は幅広く、大域しきい値(Otsu / Li / Yen)、局所適応しきい値(Sauvola / Niblack / 適応ガウス)、エッジ抽出(Canny / LoG ゼロ交差)、局所ピーク・極値のマーカー検出、領域成長・ 分水嶺(watershed / pouring)、特徴空間クラスタリング(k-means / GMM / mean-shift)、グラフ分割 (normalized-cut / GrabCut / random-walker)、過分割スーパーピクセル(SLIC / Felzenszwalb)、 能動輪郭(Chan-Vese)までを一列に並べています。どれも同じ呼び出し形 fullseye.apply(img, "name", a, b) で叩けて、つまみ a, b ∈ [0,1] が各手法の主要パラメータ(しきい値・平滑量・窓サイズ・粗さ)へ 写像されます。

出力型の例外(正直に): 次の 3 op だけは out_sort = image(二値マスクではなく多値/連続の画像)を 返します — xsk2_multiotsu(多値ラベル画像、≥3 階調)、xcv2_meanshift(mean-shift 平滑画像)、 it_region_to_bin(領域を二値画像へレンダリング)。残りはすべて {0,1} の region です。

代表的なパイプライン(op の繋がり)

セグメンテーション op は多くが「image を受けて region を出す」終端です。したがって典型的な連鎖は 前段の平滑化(smoothing 族)→ 本族の 1 手法 → 後段の region モルフォロジ/計測(region・features 族) というデータ種の受け渡しになります。

flowchart LR
  IMG["grayscale image<br/>2-D float in [0,1]"] --> PRE["pre-smoothing<br/>gauss_filter / median<br/>(smoothing 族)"]
  PRE --> SEG{"segmentation<br/>この族"}
  SEG -->|"大域しきい値<br/>otsu / threshold / sk_li"| REG["binary region<br/>2-D {0,1} mask"]
  SEG -->|"局所しきい値<br/>sk_sauvola / dyn_threshold"| REG
  SEG -->|"エッジ<br/>canny / edges_image / zero_crossing"| BND["edge / boundary region"]
  SEG -->|"分水嶺・過分割<br/>watersheds / sg_slic_superpixels"| BND
  REG --> POST["region モルフォロジ・計測<br/>(region / features 族)"]
  BND --> POST

同じ入力でも手法で頑健性が変わる、という「使い分け」の連鎖:

flowchart LR
  A["不均一照明の文書画像"] -->|"otsu(大域)"| B["割れやすい二値領域"]
  A -->|"sk_sauvola(局所適応)"| C["照明ムラに強い二値領域"]
  D["二峰性の物体画像"] -->|"otsu"| E["前景 = 物体マスク"]
  E -->|"fill_holes → blob_count<br/>(下流 region 族)"| F["個数・面積などの計測値"]

使い方(op グループ別)

呼び出しは全 op 共通で fullseye.apply(img, "<name>", a, b)(a, b ∈ [0,1]、既定 0.5)。 region 出力は入力と同形の {0,1} 二値 float64。HALCON 別名がある場合は「≈ HALCON名」で併記。

大域しきい値(1 つの閾値で前景/背景)

局所適応しきい値(照明ムラ・文書に強い)

エッジ → 領域(境界を前景化)

局所ピーク / 極値(マーカー検出)

領域成長 / 分水嶺(連結で塗り広げる)

特徴空間クラスタリング / グラフ分割

過分割スーパーピクセル / 能動輪郭

変換(領域 ↔ 画像)

動く最小例(検証済み gallery2d_segmentation から)

repo 直下で py -3.11 <this>.py として実行可能。検証済み例 examples/gallery2d_segmentation.py の GT(ground truth)チェックを公開ファサード fullseye.apply で書き直した自己完結コード。各 op が 「動く」ではなく「正しく効く」ことを beat-the-null で確かめ、最後に PASS を印字する。

# repo 直下で: py -3.11 this_file.py
import warnings
import numpy as np
import fullseye

warnings.filterwarnings("ignore")  # 各バックエンドの境界警告は本題ではない

# --- GT1: threshold は v>a。閾値 a を上げると前景が単調に減る(横勾配画像) ---
grad = np.tile(np.linspace(0, 1, 48), (48, 1))
fg_lo = fullseye.apply(grad, "threshold", 0.25, 0.0)
fg_hi = fullseye.apply(grad, "threshold", 0.75, 0.0)
assert set(np.unique(fg_lo).tolist()) <= {0.0, 1.0}          # region は二値
assert fg_lo.sum() > fg_hi.sum()                             # 単調(無関係なら差 0 = beat-the-null)
assert float(grad[fg_lo == 1].min()) > 0.25                  # 前景は必ず a 超

# --- GT2: otsu は二峰性を谷で分離。前景平均>背景平均、真円との IoU≈1 ---
n = 48
yy, xx = np.mgrid[0:n, 0:n]
disk = ((yy - 24) ** 2 + (xx - 24) ** 2) < 12 ** 2
rng = np.random.default_rng(1)
bim = np.clip(np.where(disk, 0.8, 0.2) + 0.02 * rng.standard_normal((n, n)), 0, 1)
reg = fullseye.apply(bim, "otsu", 0.0, 0.0)
fg_mean, bg_mean = float(bim[reg == 1].mean()), float(bim[reg == 0].mean())
iou = np.logical_and(reg == 1, disk).sum() / np.logical_or(reg == 1, disk).sum()
assert fg_mean - bg_mean > 0.4                               # ランダム分割なら ≈0
assert iou > 0.9                                             # 真の物体をほぼ復元

# --- GT3: canny は鋭いエッジ近傍だけ応答。平坦部は 0 ---
edge = np.full((48, 48), 0.1)
edge[:, 24:] = 0.9
ce = fullseye.apply(edge, "canny", 0.2, 0.2)
band = float(ce[:, 22:27].sum())                             # 境界列の帯
flat = float(ce[5:15, 3:13].sum())                           # 平坦パッチ
assert band > 0 and flat == 0.0 and band > 10 * (flat + 1)

# --- GT4: local_max は明るいピークだけ検出、平坦背景は拾わない ---
peak = np.full((48, 48), 0.2)
peak[24, 24] = 1.0
lm = fullseye.apply(peak, "local_max", 0.3, 0.5)             # 閾値 = 0.3 + 0.4*0.5 = 0.5
assert lm[24, 24] == 1.0                                     # ピークは検出
assert float(lm[0:10, 0:10].sum()) == 0.0                    # 平坦背景は 0
assert float(lm.sum()) < 5                                   # 疎な検出

# --- GT5: 系統ごとの代表 op が有限・同形・二値 region を返す(一般契約) ---
for name in ["binary_threshold", "sk_li", "sk_yen", "sk_sauvola", "dyn_threshold",
             "sk_canny", "edges_image", "watersheds", "regiongrowing",
             "sg_slic_superpixels", "sg_kmeans_intensity", "sg_watershed_gradient"]:
    out = fullseye.apply(bim, name, 0.5, 0.5)
    assert out.shape == bim.shape, name
    assert np.all(np.isfinite(out)), name
    assert set(np.unique(np.round(out, 6)).tolist()) <= {0.0, 1.0}, name

print("PASS")

数式(必要な op のみ)

Otsu(otsu / binary_threshold / sk_otsu / cv_otsu) — しきい値 $t$ で二分したときの クラス間分散を最大化する:

\[t^\star = \arg\max_{t}\; \sigma_B^2(t),\qquad \sigma_B^2(t) = \omega_0(t)\,\omega_1(t)\,\bigl[\mu_0(t) - \mu_1(t)\bigr]^2\]

ここで $\omega_0(t)=\sum_{i\le t} p_i$ は背景の画素比率、$\mu_0,\mu_1$ は各クラスの平均輝度。 実装は等価な形 $\sigma_B^2(t) = \dfrac{\bigl[\mu_T\,\omega(t) - \mu(t)\bigr]^2}{\omega(t)\,\bigl(1-\omega(t)\bigr)}$ を全 $t$ で評価して最大の $t$ を選ぶ。

固定/適応しきい値 — 前景領域は集合として定義される。固定 threshold:

\[R = \{\,x : v(x) > a\,\}\]

局所適応(dyn_threshold / cv_adaptive_mean)は局所平均 $\bar v_W$ に対する差分:

\[R = \{\,x : v(x) > (\bar v_W * v)(x) + c\,\}\]

Sauvola / Niblack(sk_sauvola / sk_niblack / var_threshold) — 窓ごとの平均 $m(x)$ と 標準偏差 $s(x)$ から局所しきい値を作る:

\[T_{\text{Niblack}}(x) = m(x) + k\,s(x),\qquad T_{\text{Sauvola}}(x) = m(x)\left[\,1 + k\left(\frac{s(x)}{R} - 1\right)\right]\]

$R$ は標準偏差のダイナミックレンジ。照明ムラのある文書で大域 Otsu より頑健。

Canny / LoG(canny / zero_crossing) — ガウス平滑後の勾配強度をしきい、あるいは ガウス・ラプラシアンのゼロ交差を取る:

\[g = \bigl\lVert \nabla (G_\sigma * I) \bigr\rVert,\quad R=\{x: g(x)>\tau\}; \qquad \text{zero-crossing of }\; \nabla^2 (G_\sigma * I)\]

ヒステリシス(hysteresis_threshold / sk_hysteresis) — 2 つのしきい値 $t_\text{low}<t_\text{high}$。 $v>t_\text{high}$ の種を残し、そこから $v>t_\text{low}$ で連結する画素だけを前景に加える(弱いエッジの 断片化を防ぐ)。

Normalized cut(sg_normalized_cut_2) — 親和行列 $W$、次数対角行列 $D$ の一般化固有問題 $(D-W)\,y = \lambda\,D\,y$ の第 2 固有ベクトル(Fiedler ベクトル)を中央値でしきって 2 分割する (Shi & Malik)。

サンプルデータ

デバッグ・動作確認には ../../SAMPLES.md の 2-D 画像源が使える。二峰性/勾配/ チェッカの合成画像(gradient / blobs / shapes / checker_noisy)はしきい値・エッジの GT 検証に、 skimage.datacoins(硬貨=Otsu/watershed)・page(文書=Sauvola/Niblack 適応二値化)・cell (細胞=領域成長/分水嶺)は実データでの挙動確認に向く。取得は import sample_images; sample_images.load('coins')

参考文献(正典)

台帳 ../../../REFERENCES.md に準拠。この族のアルゴリズムの古典:


© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.