5.26. 線とセグメントの検出¶
シーンの特徴の中には、つながった色の領域ではなく、向きをもった直線的なエッジであるものもあります。床に塗られたライン、2 つの面の間の継ぎ目、印刷された矩形の辺、出入口の縁などです。これらをブロブ検出器に見つけさせるのは間違った問いかけです。エッジは幅 1 ピクセルしかなく、ブロブアルゴリズムは色を伴う面積を求めるため、答えは空であるかノイズだらけで返ってきます。
向きをもったエッジに適した検出器はHough 線変換です。image モジュールはこれを 2 つの形で公開しています。find_lines() は無限の線を返します(すべての線は画像全体にわたって延長されます)。find_line_segments() は有限のセグメントを返します(各線はフレーム内に端点を持ちます)。アプリケーションがどちらを必要とするかは、対象のエッジがフレーム全体にわたって連続しているか、その一部にしかまたがっていないかによります。
5.26.1. Hough 変換の仕組み¶
どちらの検出器も同じ中核的な考え方を共有しているので、一度理解しておく価値があります。image モジュールはまず入力に Sobel 風のエッジフィルタを実行し、各ピクセルが向きをもったエッジ上にある可能性の高さでスコア付けします。次に、そうした各エッジピクセルは、それが乗っているかもしれないすべての線に投票します。最も多くの投票を集めた線が勝ちます。
線はHough 空間において 2 つの数値でパラメータ化されます。theta は線の角度(0 〜 179 度)、rho は画像の原点から線までの垂直距離(符号付き、ピクセル単位)です。画像に含まれるすべての線は、(theta, rho) 空間内の 1 点になります。入力内の各エッジピクセルは、その位置と整合するすべての (theta, rho) の組み合わせに 1 票を投じます。概念的には、Hough 空間を通る曲線です。そうした曲線が多く交差する場所では、多くのエッジピクセルが同じ線について一致しており、その交点が検出となります。
検出器は、投票総数がしきい値を超える Hough 空間内の局所最大値を返します。返される各 Line は両方の表現を持ちます。端点形式の x1, y1, x2, y2(無限の場合は画像の境界にクリップされます)、Hough 形式の theta, rho、そしてサイズと投票数をそれぞれ表す length と magnitude です。
5.26.2. 無限の線¶
find_lines() は Hough 変換を実行し、最も強い線を、それぞれ画像全体にわたって延長して返します。
lines = img.find_lines(threshold=1500, theta_margin=25, rho_margin=25)
for l in lines:
img.draw_line(l, color=(255, 0, 0))
threshold は、線が受け入れられるための投票総数の最小値です。投票総数は寄与した各ピクセルの Sobel エッジの大きさを合計したものなので、threshold の値を大きくすると、通過するにはより長いまたはより強いエッジが要求されます。そのため、適切な値は画像の解像度(解像度が高いほど、より長い線はより多くの投票を蓄積します)とシーンの両方に依存し、個々のアプリケーションに合わせて調整しなければなりません。調整の出発点のおおまかな目安としては、クリアな画像内の中程度の線には 1000、弱いコントラストや短い線には 500 以下、エッジノイズのクラスタから誤検出の線が形成されるごちゃごちゃしたシーンには 2000 以上です。
theta_margin と rho_margin は、近接した最大値のマージを制御します。1 つの物理的なエッジは、その真の (theta, rho) の周りに高投票ビンの小さなクラスタを生じさせ、検出器は返す前に各クラスタをそのピークに集約します。theta_margin=25(度)は、向きが 25 度以内のピークをマージします。rho_margin=25(ピクセル)は、距離が 25 ピクセル以内のピークをマージします。デフォルト値は妥当です。値を上げるとより少なくより明確に区別された線が返され、下げるとより多く、ときに重複した線が返されます。
x_stride と y_stride は、find_blobs() がピクセルをステップ処理するのと同じように、投票中にエッジピクセルをステップ処理します。デフォルトの 2 と 1 は一般的なケースで機能します。これらを上げると解像度を犠牲にして探索が高速化します。roi は探索をフレームの一領域に制限し、返される線を絞り込むと同時に処理量を減らします。
返される各線は直接描画できます。Line オブジェクトはそのまま draw_line() に渡され、その先頭から (x1, y1, x2, y2) の端点フィールドが読み取られます。l.theta は度単位の角度で、1 回の比較で線を水平、垂直、または斜めに分類します。l.magnitude は投票総数で、返された線を最も強いものから最も弱いものへと並べ替えます。
5.26.3. 線セグメント¶
find_lines() はフレーム全体にまたがるエッジに適した検出器ですが、実世界の多くのエッジ(印刷されたバーコードの左辺、ラベルの上辺、定規の見えている辺など)は画像の一部にしか走っていません。find_line_segments() は、端点がフレーム内にある有限のセグメントを返します。
segments = img.find_line_segments(merge_distance=5, max_theta_difference=10)
for s in segments:
img.draw_line(s, color=(0, 255, 0))
セグメント検出器は、Hough 空間で投票するのではなく、向きをもったエッジピクセルに沿って直接たどります。その結果は短い直線的な区間の集まりになります。merge_distance は、2 つの同一直線上の短い区間がまたいでも 1 つの返されるセグメントにマージできる最大のピクセル間隔を設定します。max_theta_difference は、マージャが隣接する区間の間で許容する向きの角度差を設定します。寛大なマージ(merge_distance=10, max_theta_difference=15)は、ときに本当に別々のエッジを橋渡ししてしまう代償を払って、少数の長いセグメントを返します。厳密なマージ(merge_distance=0, max_theta_difference=5)は多くの短いセグメントを返し、Python 側でそれらを整理させます。
結果オブジェクトは find_lines() が返すのと同じ Line 型で、同じプロパティを持つため、パイプラインはどちらの種類の検出も同じ下流のコードパスで処理できます。唯一の実用上の違いは、セグメントの端点が画像内の線の実際の端であるのに対し、無限の線の端点は線が画像の境界を横切る場所であることです。
5.26.4. それぞれをいつ使うか¶
2 つの手法のどちらを選ぶかは、1 つの問いに帰着します。アプリケーションは線がどこで止まるかを気にするか? です。
find_lines() は、答えがノーのときに適したツールです。ライン追従ロボットは線がどちらに向かっているかとフレームの下端をどこで横切るかを知る必要があります。線そのものは地平線やその先まで走っています。地平線検出器は画像内で最も強い向きをもったエッジを求めますが、地平線がどこで終わるかを知る必要はありません。
find_line_segments() は、答えがイエスのときに適したツールです。印刷された矩形の 4 辺を特定するには、端点が分かっている 4 つのセグメントが必要です。ディスプレイを指す指を追跡するということは、端点が指の先端と付け根である短いセグメントをたどることを意味します。見えている傷の長さを測定するには、セグメントのピクセル単位の実際の範囲が必要です。
どちらの検出器も共通の制約を持ちます。それはコントラストを必要とすることです。これらが基盤とする Sobel エッジフィルタは明るさの勾配に反応します。同じ明るさの背景に対する色付きのエッジ(同じ輝度の緑の壁の上の赤い線)は、勾配を生じず検出もされません。実際にそのケースが現れたときの対処法は、探索の前に適切なコントラストを持つ単一の LAB チャンネルをグレースケール画像として抽出することです。b チャンネルを選択した to_grayscale() は、輝度チャンネルだけでは平坦になる場合でも緑に対する赤を分離します。そのチャンネル画像を線検出器に渡してください。