この族は、画像から 輪郭(contour) を下位画素(sub-pixel)精度で取り出し、それを選別・平滑・変換・領域化する XLD 系(HALCON 流の eXtended Line Description)の道具に、1 次元キャリパ計測(measure1d) と テンプレート照合(matching) を加えた計測寄りのファミリです。入力は基本的に 1 枚のグレースケール画像([0,1] の 2 次元 float。一部はカラー H×W×3 や、既に得た輪郭 dict / 領域マスク)で、出力は用途に応じて 4 種に分かれます — 輪郭({"shape": (H,W), "cs": [ (N,2) 点列, ... ]}、点は (row, col))、領域([0,1] の 2 値マスク)、特徴(有限スカラ)、照合結果([score, row, col, ...] の配列)。
平たく言えば「エッジや線をきれいな点列として取り出し → 短いゴミを捨て・平滑し・座標変換し → 領域に焼く/点数や長さを測る」という輪郭の一生と、「計測線 1 本に沿ってエッジ位置・本数・しきい値交差を測る」キャリパ、「テンプレートを画像内で探す」照合を、すべて同じ fullseye.apply(img, "op名", a, b) の呼び出しモデルで扱えるようにしたものです。2-D パイプライン op は「1 画像 + 2 つのつまみ a, b ∈ [0,1]」で、a は多くの場合しきい値/スケール、b は op ごとの二次パラメータ(計測線の閾値・オフセットなど)です。
輪郭の抽出 → 整形 → 領域化/計測という典型的な連鎖です。データ種(image / contour / region / feature)が op をまたいで繋がります。
flowchart LR
IMG[image<br/>グレー画像] -->|edges_sub_pix| C0[contour<br/>下位画素エッジ]
IMG -->|threshold_sub_pix| C0
IMG -->|lines_gauss| C0
C0 -->|select_contours<br/>短い輪郭を捨てる| C1[contour]
C1 -->|smooth_contours<br/>移動平均| C2[contour]
C2 -->|fit_line_contours<br/>直線当てはめ| C3[contour]
C2 -->|contours_to_region| REG[region<br/>2値マスク]
C2 -->|contour_point_num_xld| F1[feature<br/>点数スカラ]
REG -->|gen_contour_region_xld| C0
flowchart LR
IMG[image] -->|m1_measure_pos| CP[contour<br/>エッジ位置]
IMG -->|m1_measure_thresh| FT[feature<br/>交差回数]
IMG -->|m1_measure_pairs| FP[feature<br/>エッジ対の数]
TPL[template<br/>set_match_template] -.-> IMG
IMG -->|ncc_locate| M1[match<br/>score,row,col]
IMG -->|shape_locate| M2[match<br/>score,row,col,angle]
呼び出しは一律 fullseye.apply(入力, "op名", a, b)。カッコ内の HALCON: は対応する HALCON オペレータ名です。
0.15+0.5a で閾値化し、連結成分を点列輪郭にしたうえで、各点を勾配の法線方向に放物線当てはめして勾配稜線へ寄せる(サブピクセル精度、Devernay 1995 系)。fullseye.apply(img, "edges_sub_pix", 0.4, 0.5)np.where の 整数画素座標そのもので、sub_pix を名乗りながらサブピクセル精度が無かった。実測(真の位置が列 20.37 の合成ステップエッジ、a=0.2): 旧実装の返す列は {20.0, 21.0} で平均絶対誤差 0.500 px、精密化後は {20.324, 20.370} で 0.0228 px。点の個数・連結成分の分け方は不変。非極大抑制はしていないので、太いエッジでは帯の全画素が稜線へ寄って重なる —— 1 画素幅の連鎖が要るなら canny、より高精度な等値線が要るなら threshold_sub_pix。0.2+0.5a の等値線を下位画素輪郭にする(HALCON: threshold_sub_pix)。fullseye.apply(img, "threshold_sub_pix", 0.5, 0.5)a が平滑スケール(HALCON: zero_crossing_sub_pix)。fullseye.apply(img, "zero_crossing_sub_pix", 0.5, 0.5)fullseye.apply(img, "lines_gauss", 0.5, 0.5)fullseye.apply(img, "lines_facet", 0.5, 0.5)0.2+0.5a)を輪郭化する(HALCON 別名なし)。fullseye.apply(img, "sk_find_contours", 0.5, 0.5)fullseye.apply(color, "edges_color_sub_pix", 0.4, 0.5)fullseye.apply(color, "lines_color", 0.5, 0.5)3+40a 未満の輪郭を捨てる(HALCON: select_contours_xld)。fullseye.apply(cs, "select_contours", 0.1, 0.5)fullseye.apply(cs, "select_contours_xld", 0.1, 0.5)fullseye.apply(cs, "select_shape_xld", 0.1, 0.5)1+3a の移動平均で点列を平滑化する(HALCON: smooth_contours_xld)。fullseye.apply(cs, "smooth_contours", 0.5, 0.5)fullseye.apply(cs, "smooth_contours_xld", 0.5, 0.5)fullseye.apply(cs, "fit_line_contours", 0.5, 0.5)fullseye.apply(cs, "close_contours_xld", 0.5, 0.5)-20°+40a° の回転アフィンを掛ける(HALCON: affine_trans_contour_xld)。fullseye.apply(cs, "affine_trans_contour_xld", 0.5, 0.5)fullseye.apply(cs, "affine_trans_polygon_xld", 0.5, 0.5)fullseye.apply(cs, "projective_trans_contour_xld", 0.5, 0.5)(r, θ) へ写す(HALCON: polar_trans_contour_xld)。fullseye.apply(cs, "polar_trans_contour_xld", 0.5, 0.5)fullseye.apply(cs, "shape_trans_xld", 0.5, 0.5)1+2a 回 dilation して領域化する(HALCON: gen_region_contour_xld)。fullseye.apply(cs, "contours_to_region", 0.5, 0.5)fullseye.apply(cs, "gen_region_contour_xld", 0.5, 0.5)fullseye.apply(cs, "gen_region_polygon_xld", 0.5, 0.5)find_contours、不在時は Moore 近傍トレース。2026-08-30 にラスタ順から修正 — 順序前提の EFD 等にそのまま渡せる)。fullseye.apply(reg, "gen_contour_region_xld", 0.5, 0.5)min(1, N/500) に正規化して返す(HALCON: contour_point_num_xld)。fullseye.apply(cs, "contour_point_num_xld", 0.5, 0.5)計測線は画像中心を通り、向きは θ = a·π。線に沿ってバイリニアで輝度プロファイルを取り、その上でエッジ(勾配ピーク)を下位画素に refine します。b の意味は op ごとに違います。
b は線の垂直オフセット(0.5=中央)(HALCON: measure_projection)。fullseye.apply(img, "m1_measure_projection", 0.5, 0.5)b は相対最小振幅(HALCON: measure_pos)。fullseye.apply(img, "m1_measure_pos", 0.5, 0.3)b を横切る回数を返す(HALCON: measure_thresh)。fullseye.apply(img, "m1_measure_thresh", 0.5, 0.5)b は相対振幅しきい値(HALCON: measure_pairs)。fullseye.apply(img, "m1_measure_pairs", 0.5, 0.3)∈[0,1] を与え、b 以上のエッジ位置を輪郭で返す(HALCON: fuzzy_measure_pos)。fullseye.apply(img, "m1_fuzzy_measure_pos", 0.5, 0.3)照合系は先に ops.set_match_template(t) で探すテンプレート([0,1] の 2 次元 float)を 同じスレッドで 設定してから呼びます。未設定なら 0 スコアの配列を返します(fail-safe)。
[score, row, col] で返す。row, col はテンプレート中心(HALCON: find_ncc_model)。ops.set_match_template(t); fullseye.apply(img, "ncc_locate", 0.5, 0.5)[score, row, col, angle] を返す(回転不変)(HALCON: find_shape_model)。ops.set_match_template(t); fullseye.apply(img, "shape_locate", 0.5, 0.5)repo 直下で py -3.11 で実行すると PASS を出力します(輪郭パイプライン・計測・照合の 3 系統を、既知の真値で確認する自己完結テスト)。
import numpy as np
import fullseye
import ops # matching ops read their template from a context set here
# --- structured test image + a flat control --------------------------------
n = 48
yy, xx = np.mgrid[0:n, 0:n].astype(float)
disk = ((yy - 24.0) ** 2 + (xx - 30.0) ** 2) < 5.0 ** 2
img = np.clip(0.2 + 0.2 * (xx / (n - 1)) + 0.7 * disk, 0, 1) # gradient bg + bright disk
flat = np.full((n, n), 0.42)
# 1) contour pipeline: sub-pixel edges -> select -> smooth -> region
edges = fullseye.apply(img, "edges_sub_pix", 0.4, 0.5) # image -> contour
assert isinstance(edges, dict) and "cs" in edges and "shape" in edges
assert len(edges["cs"]) > 0 # structure => contours
assert len(fullseye.apply(flat, "edges_sub_pix", 0.4, 0.5)["cs"]) == 0 # beat-the-null
kept = fullseye.apply(edges, "select_contours", 0.1, 0.5) # contour -> contour
assert len(kept["cs"]) <= len(edges["cs"]) # selection never adds
smoothed = fullseye.apply(kept, "smooth_contours", 0.5, 0.5) # contour -> contour
region = np.asarray(fullseye.apply(smoothed, "contours_to_region", 0.5, 0.5)) # -> region
assert set(np.unique(region).tolist()).issubset({0.0, 1.0}) # binary region
assert region.sum() > 0 # non-empty
# 2) 1-D caliper measurement: the projection recovers a constant profile
lo = fullseye.apply(np.full((n, n), 0.2), "m1_measure_projection", 0.5, 0.5)
hi = fullseye.apply(np.full((n, n), 0.8), "m1_measure_projection", 0.5, 0.5)
assert abs(lo - 0.2) < 0.05 and abs(hi - 0.8) < 0.05
assert fullseye.apply(flat, "m1_measure_thresh", 0.5, 0.5) == 0 # no crossings on flat
# 3) template matching: locate a known patch (structured, not flat)
tmpl = img[18:30, 24:36].copy() # 12x12 crop around the disk (has structure)
ops.set_match_template(tmpl) # NCC/shape ops read this template
score, row, col = fullseye.apply(img, "ncc_locate", 0.5, 0.5) # image -> match
assert score > 0.99 # a crop matches itself near-perfectly
assert abs(row - 24.0) < 2 and abs(col - 30.0) < 2 # at the disk centre
print("PASS")
正規化相互相関(ncc_locate / shape_locate の核) — 位置 (y,x) のスコアは、テンプレート T とそこに重なる同サイズ窓 I_w の Pearson 相関で、輝度・コントラスト不変(完全一致で 1):
分母のテンプレート側エネルギーが 0(平坦テンプレート)だとスコアは 0 になります — 照合には構造のあるテンプレートが必要です。
下位画素エッジの放物線 refine(measure1d のエッジ位置) — 勾配振幅 g のピーク近傍 3 点に放物線を当て、頂点オフセット δ でサブ画素位置 i+δ を得ます:
輪郭のアフィン変換(affine_trans_contour_xld) — 画像中心 c=(y_c,x_c) まわりに回転 R(φ)(φ = -20°+40a°)を掛けます:
ファジー振幅メンバシップ(m1_fuzzy_measure_pos) — 振幅 amp を下限 lo=0.05\,g_{\max} と最大 g_{\max} の間で正規化したスコアが b 以上のエッジを残します:
この族のデバッグには 2-D 画像が使えます(外部 DL 不要)。輪郭・エッジ抽出には skimage の coins / camera / page(境界がはっきりした自然画像)、キャリパ計測とテンプレート照合には合成の shapes / checker_noisy(既知の段差・矩形を持つ)が向きます。取得法とライセンスは ../../SAMPLES.md を参照(import sample_images; sample_images.load('coins'))。
台帳は ../../../REFERENCES.md。この族のアルゴリズムの古典:
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.