fullseye

テクスチャ・周波数・分解 — 使い方ガイド

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

この族は「模様・周期・照明/反射の分離」を測るための 2-D オペレータ 48 本の道具箱です。入力は一枚のグレー画像(float64、[0,1])、つまみは 2 つのスカラ a,b ∈ [0,1]。出力は用途に応じて次の 3 種になります。画像→画像(スペクトル、フィルタ済み画像、テクスチャ応答マップ、分解した各層)が大半で、cooc_feature_matrix だけは 画像→特徴スカラ([0,1] の GLCM Haralick 値)を返します。

大きく 3 つのカテゴリに分かれます。texture(局所統計・エントロピ・Gabor/LBP/Laws・リッジ強調・census/rank)は「そこに模様があるか、どの向きか」を画素ごとに応答へ変える。frequency(FFT/DCT/wavelet のスペクトル、ローパス/ハイパス/バンドパス、Radon)は画像を周波数の言葉に置き換えて、帯域で通したり落としたりする。decomposition(構造+テクスチャ、RPCA の低ランク+疎、Retinex、homomorphic、局所コントラスト正規化)は一枚の画像を「滑らかな背景/照明」と「その上のディテール/欠陥」に分ける。工業表面検査では 欠陥 とはまさに背景モデルに属さない残差のことなので、この分解層がそのまま検査対象になります。値域は [0,1] に固定されません — ハイパス/バンドパス/逆FFT は負のディテールを持つのが正しく、実装は零点を 0.5 に写す signed01 正規化で負の半分を保存します(グラウンドトゥルース検査済み: examples/gallery2d_texture_freq.py)。

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

周波数側は「スペクトルを覗く」経路と「帯域で空間フィルタする」経路に分かれます。

flowchart LR
    I["image<br/>gray [0,1]"] --> FFT["fft_image / power_ln<br/>log パワースペクトル"] --> SPEC["image<br/>(スペクトル表示)"]
    I --> LP["lowpass"] --> B["image<br/>(ぼかし=低域)"]
    I --> HP["highpass"] --> D["image<br/>(エッジ=高域, 零点0.5)"]
    I --> BP["bandpass_image"] --> M["image<br/>(中域のみ)"]

テクスチャ・分解側は「背景と残差に割る → 残差のエネルギを測る → しきい値で欠陥領域にする」と繋がり、下流の segmentation 族へ橋渡しされます。

flowchart LR
    I["image"] --> ST["dc_structure_texture"] --> S["image<br/>(構造/cartoon)"]
    I --> TR["dc_texture_residual"] --> T["image<br/>(テクスチャ層 @0.5)"]
    T --> SF["std_filter / entropy_image"] --> MAP["image<br/>(テクスチャ・エネルギ地図)"]
    MAP --> TH["threshold<br/>(segmentation 族)"] --> R["region<br/>(欠陥マスク)"]

使い方(op グループ別)

呼び出しは全 op 共通で fullseye.apply(img, "<name>", a, b)a,b 省略時は 0.5。以下、各 op を「名前 — 何をするか — 呼び出し例」で。

周波数変換とスペクトル(FFT / DCT / wavelet / Radon)

周波数フィルタ(帯域で通す/落とす)

★正規化の規約(2026-09-02 に統一)。帯域を落とした結果は 零平均の符号つき信号なので、highpass / highpass_image / bandpass_image / fft_image_inv は零点を 0.5 に置く signed01[0,1] に写す。それ以前は highpass_image / bandpass_image / fft_image_inv だけが最大絶対値で割る _norm を使っており、image を名乗りながら値域 [-1,1] の配列を返していた(実測 camera.png, a=0.2: highpass_image min=-0.6067・負 50.2%、bandpass_image min=-0.8812・負 49.8%、fft_image_inv 負 49.4%)。保存・表示すると画素の約半分が無言で真っ黒に潰れる。非負の応答(fft_image / power_real / power_byte / lowpass)は従来どおり。

テクスチャ記述(局所統計・エントロピ・対称性)

方向性・リッジ・畳み込み系テクスチャフィルタ

★2026-09-02 まで gabor / gen_gabor / hx_gabor画像ごとの正規化(最大絶対値で割る _normhx_gabor は min–max 引き伸ばし)で、向きによる応答の大小が消えていた。実測(96×96 の横縞): 生の畳み込みの平均振幅は θ=0° が 0.0165、θ=90° が 0.9077 で 54.9 倍の差があるのに、_norm 経由の平均は 0.3554 対 0.4790 = 1.35 倍まで潰れていた。hx_gabor はさらに悪く 順序が逆転(横縞画像で a=0.5 が 0.34663、ほぼ反応しないはずの a=0 が 0.58434)。カーネル L1 で割る固定スケールにしたので 54.9 倍(hx_gabor は 21.0 倍)がそのまま残る。sk_gabor は向きノブを持たない(skimage 既定 θ=0、a は周波数)ため今回の対象外で、いまも画像依存の _norm を使っている —— 画像を跨いだ絶対比較には使えない。

順序不変(census / rank、照明ゲインに頑健)

構造+テクスチャ / 低ランク+疎の分解(検査向け)

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

repo 直下で py -3.11 <保存名>.py を実行すると、周波数フィルタ・テクスチャ統計・順序不変性・分解の再構成をグラウンドトゥルースで検証し、最後に PASS を出力します。検証済みギャラリー examples/gallery2d_texture_freq.py の GT を土台にした自己完結コードです。

import os, sys
sys.path.insert(0, os.getcwd())          # imgevolve リポジトリ直下から実行する
import numpy as np
import fullseye


def lapvar(z):
    """高周波エネルギの近似: 4 近傍ラプラシアンの分散。"""
    lap = z[2:, 1:-1] + z[:-2, 1:-1] + z[1:-1, 2:] + z[1:-1, :-2] - 4 * z[1:-1, 1:-1]
    return float((lap ** 2).mean())


# --- テスト画像を合成(勾配 + 円盤 + 市松 + 微小ノイズ)と補助パターン ---
n = 64
yy, xx = np.mgrid[0:n, 0:n].astype(np.float64)
rng = np.random.default_rng(20260812)
disk = ((yy - n * 0.35) ** 2 + (xx - n * 0.4) ** 2) < (n * 0.18) ** 2
checker = ((xx.astype(int) // 6 + yy.astype(int) // 6) % 2) * 0.15
img = np.clip(0.35 * (xx / (n - 1)) + 0.45 * disk + checker
              + 0.03 * rng.standard_normal((n, n)), 0, 1)
step = np.where(xx < n / 2, 0.2, 0.8)          # 中央に鋭い縦エッジ
flat = np.full((n, n), 0.4)                    # 平坦
tex = np.clip(0.4 + 0.25 * rng.standard_normal((n, n)), 0, 1)  # テクスチャ

# 1) lowpass は高周波エネルギを半分未満に落とす(ぼかし)
assert lapvar(fullseye.apply(img, "lowpass", 0.5, 0.5)) < 0.5 * lapvar(img)

# 2) highpass はエッジ上で平坦部よりはるかに強く応答する(0.5 = ディテール無し)
hp = fullseye.apply(step, "highpass", 0.5, 0.5)
col = n // 2
edge = float(np.abs(hp[:, col - 1:col + 1] - 0.5).mean())
flat_e = float(np.abs(hp[:, 2:6] - 0.5).mean())
assert edge > 3.0 * flat_e

# 3) std_filter: 平坦部の局所標準偏差 ≈ 0、テクスチャ部で大
assert float(fullseye.apply(flat, "std_filter", 0.5, 0.5).mean()) < 1e-3
assert float(fullseye.apply(tex, "std_filter", 0.5, 0.5).mean()) > 0.1

# 4) entropy_image: 定数画像 ≈ 0、ノイズ/テクスチャで高い
assert float(fullseye.apply(flat, "entropy_image", 0.5, 0.5).mean()) < 1e-6
assert float(fullseye.apply(tex, "entropy_image", 0.5, 0.5).mean()) > 0.3

# 5) tf_rank_transform: 正のゲインに不変(順序のみ依存)、コントラスト反転では変化
r1 = fullseye.apply(img, "tf_rank_transform", 0.5, 0.0)
assert np.array_equal(r1, fullseye.apply(0.6 * img, "tf_rank_transform", 0.5, 0.0))
assert not np.array_equal(r1, fullseye.apply(1.0 - img, "tf_rank_transform", 0.5, 0.0))

# 6) xsp_dct_lowpass も高周波エネルギを半減以下にする(周波数領域のぼかし)
assert lapvar(fullseye.apply(img, "xsp_dct_lowpass", 0.5, 0.5)) < 0.5 * lapvar(img)

# 7) 構造 + (テクスチャ - 0.5) は入力を再構成する(飽和しない画素で厳密)
s = fullseye.apply(img, "dc_structure_texture", 0.5, 0.5)
t = fullseye.apply(img, "dc_texture_residual", 0.5, 0.5)
unsat = (t > 1e-9) & (t < 1 - 1e-9)
assert np.allclose((s + (t - 0.5))[unsat], img[unsat], atol=1e-6)

# 8) cooc_feature_matrix は [0,1] のスカラ特徴を返す(画像 → 特徴)
c = float(fullseye.apply(img, "cooc_feature_matrix", 0.5, 0.5))
assert 0.0 <= c <= 1.0

print("PASS")

数式(必要な op のみ)

パワースペクトル(fft_image / fft_generic / power_ln / power_byte)は FFT の対数振幅で、$S(u,v) = \log!\bigl(1 + \mathcal{F}{I}(u,v) \bigr)$。位相(phase_rad / phase_deg)は $\phi = \dfrac{\angle \mathcal{F}{I} + \pi}{2\pi} \in [0,1]$。

周波数フィルタ(lowpass / highpass / bandpass_image)は放射周波数 $r=\sqrt{f_u^2+f_v^2}$ のマスク乗算と逆変換:

\[\hat{I} = \mathcal{F}^{-1}\!\bigl\{ \mathcal{F}\{I\}\cdot M(r) \bigr\},\quad M_{\text{lp}}(r)=\mathbb{1}[r\le r_0],\;\; M_{\text{hp}}(r)=\mathbb{1}[r> r_0],\;\; M_{\text{bp}}(r)=\mathbb{1}[r_{\text{lo}}<r<r_{\text{hi}}].\]

局所標準偏差(std_filter / deviation_image)は窓 $w$ 上で $\sigma_w=\sqrt{\overline{I^2}-\bar{I}^2}$。局所コントラスト正規化(dc_local_contrast_norm)は $\dfrac{I-\mu_w}{\sigma_w+\epsilon}$ を 0.5 中心へ。

局所エントロピ(entropy_image / sk_entropy)は局所ヒストグラム ${p_k}$ に対し $H=-\sum_k p_k\log_2 p_k$。GLCM energy(cooc_feature_matrix)は正規化共起行列 $P$ の角二次モーメント $E=\sum_{i,j} P(i,j)^2$。

Gabor カーネル(gabor / gen_gabor)は $g(x,y)=e^{-(x^2+y^2)/2\sigma^2}\cos!\bigl(2\pi f\,x_\theta\bigr)$($x_\theta=x\cos\theta+y\sin\theta$)を DC 除去 $g\leftarrow g-\bar{g}$ して帯域通過にしたもの。構造テンソルのコヒーレンス(xsk_struct_coherence)は固有値 $\lambda_1\ge\lambda_2$ から $C=\dfrac{\lambda_1-\lambda_2}{\lambda_1+\lambda_2}$。

Retinex 反射率(dc_retinex)は $R=\log I-\log(G_\sigma * I)$。構造/テクスチャ分解(dc_structure_texture / dc_texture_residual)は ROF の TV-L2 問題 $\min_u \dfrac{1}{2\lambda}\lVert u-I\rVert_2^2+\mathrm{TV}(u)$ の解 $u$(構造)と残差 $I-u$(テクスチャ)。Robust PCA(dc_rpca_lowrank / dc_rpca_sparse)は Principal Component Pursuit $\min_{L,S}\lVert L\rVert_*+\lambda\lVert S\rVert_1 \ \text{s.t.}\ L+S=I$ の低ランク $L$ と疎 $S$。

サンプルデータ

デバッグには ../../SAMPLES.md の 2-D 画像源を使えます。合成の checker_noisygrain_synthweave_synthbrick_quilt は周期/テクスチャ応答(Gabor/LBP/std_filter/cooc_feature_matrix)や帯域フィルタの確認に、skimage.datacameracoins(BSD/public domain)は構造+テクスチャ分解や FFT スペクトルの確認に向きます。取得は import sample_images; sample_images.load("<name>")

参考文献(正典)

台帳 ../../../REFERENCES.md。DOI は付さず Author Year, “Title” 形式。


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