道路標示認識のためのOpenCVの紹介

こんにちは、Habr! ディープラーニングの卒業生であり、ビッグデータプログラムコーディネーターであるCyril Danilyukから、OpenCVコンピュータービジョンフレームワークを使用して路面標示線を定義した経験についての資料を公開しています。



画像






少し前に、私はUdacityからプログラムを開始しました: “ Self-Driving Car Engineer Nanodegree” 。 オートパイロットで駆動システムを構築するさまざまな側面に関する多くのプロジェクトで構成されています。 最初のプロジェクト、つまり路面標示の単純な線形検出器に対する私の決定を提示します。 最後に何が起こったのかを理解するには、まずビデオを見てください:







このプロジェクトの目標は、レーンのレーンごとの認識のための単純な線形モデルを構築することです:入力でフレームを取得し、一連の変換を行います。これについては後で説明し、処理します。 プロジェクトは意図的にシンプルです。線形モデルのみ、良好な気象条件と視認性のみ、2本のマーキングラインのみです。 当然、これは実稼働ソリューションではありませんが、そのようなプロジェクトでもOpenCV、フィルターで十分に遊ぶことができ、一般に、自動車の自動操縦開発者が直面する困難を感じるのに役立ちます。



検出器の動作原理



検出器の構築プロセスは、3つの主要なステップで構成されています。



  1. データの前処理、ノイズフィルタリング、画像のベクトル化。
  2. 最初のステップのデータに応じて路面標示線のステータスを更新します。
  3. 元の画像に更新された線やその他のオブジェクトを描画します。


最初に、3チャンネルRGB画像がimage_pipeline



関数の入力に送られ、それがフィルター処理、変換され、 Line



およびLane



オブジェクトが関数内で更新されます。 次に、以下に示すように、必要なすべての要素が画像自体の上に描画されます。



画像






ほとんどの分析タスクとは異なり、私はOOPスタイルでタスクにアプローチしようとしました。その結果、各ステップが他のステップから分離されることが判明しました。



ステップ1:前処理とベクトル化



私たちの作業の最初の段階は、データサイエンティストと生データを扱うすべての人になじみがあります。まず、データを前処理し、次にアルゴリズムで明確な方法でベクトル化する必要があります。 ソース画像の前処理とベクトル化の一般的なパイプラインは次のとおりです。



 blank_image = np.zeros_like(image) hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) binary_mask = get_lane_lines_mask(hsv_image, [WHITE_LINES, YELLOW_LINES]) masked_image = draw_binary_mask(binary_mask, hsv_image) edges_mask = canny(masked_image, 280, 360) # Correct initialization is important, we cheat only once here! if not Lane.lines_exist(): edges_mask = region_of_interest(edges_mask, ROI_VERTICES) segments = hough_line_transform(edges_mask, 1, math.pi / 180, 5, 5,
      
      









私たちのプロジェクトは、行列演算を使用してピクセルレベルで画像を操作するための最も一般的なフレームワークの1つであるOpenCVを使用します。



最初に、元のRGBイメージをHSVに変換します-特定の色の範囲を強調表示するのに便利なのはこのカラーモデルです(そして、レーンを決定するために黄色と白の色合いに興味があります)。



以下のスクリーンショットに注意してください。RGBで「すべて黄色」を強調表示することは、HSVよりもはるかに困難です。



画像






画像をHSVに変換した後、ガウスぼかしを適用することを推奨する人もいますが、私の場合、認識の質が低下しました。 次の段階は二値化です(画像を、興味のある色のバイナリマスクに変換します:黄色と白の色合い)。



画像






最後に、画像をベクトル化する準備が整いました。 2つの変換を適用します。



  1. Canny Border Detector :画像強度勾配を計算し、2つのしきい値を使用して弱い境界を削除し、目的の境界(( canny



    )を使用)をcanny



    関数のしきい値として使用する最適な境界検出アルゴリズム。
  2. ハフ変換:Cannyアルゴリズムを使用して境界を取得したら、線を使用して境界を接続できます。 私はアルゴリズムの数学に行きたくありません-別の投稿に値します-このリンクまたは上記のリンクは、メソッドに興味がある場合に役立ちます。 主なことは、この変換を適用すると、一連の線を取得し、各線が少しの追加処理とフィルタリングを経て、既知の傾斜角と自由項を持つLineクラスのインスタンスになることです。








明らかに、画像の上部にマーキング線が含まれている可能性は低いため、無視できます。 2つの方法があります:すぐにバイナリマスクの上部を黒でペイントするか、よりスマートなラインフィルタリングを考えます。 私は2番目の方法を選択しました。地平線より上にあるすべてのものをマーキングラインにすることはできないと考えました。



スカイライン(消失点)は、右車線と左車線が収束する点によって決定できます。



ステップ2:道路標示線を更新する



道路標示線は、最後のステップ(実際にはハフ変換からのLine



オブジェクト)からsegments



オブジェクトを受け取るimage_pipeline



update_lane(segments)



関数を使用して更新されます。



プロセスを容易にするために、OOPを使用し、 Lane



クラスのインスタンスとして道路標示線を表すことにしました: Lane.left_line, Lane.right_line



。 一部の学生は、グローバル名前空間に「lane」オブジェクトを追加することに限定していますが、私はコード内のグローバル変数のファンではありません。



Lane



クラスとLine



クラスとそのインスタンスを詳しく見てみましょう。



Line



クラスの各インスタンスは、個別の線を表します:道路標示の一部またはハフ変換によって決定される任意の線だけです。一方、 Lane



クラスのオブジェクトの主な目的は、この線が道路標示のセグメントであるかどうかを識別することです。 これを行うために、次のロジックにガイドされます。



  1. 線を水平にすることはできず、緩やかな勾配が必要です。
  2. 道路標示線と候補線の勾配の差が大きくなりすぎないようにしてください。
  3. 候補線は、それが属する道路標示から遠く離れてはなりません。
  4. 候補線は地平線の下にある必要があります


したがって、マーキングラインが属しているかどうかを判断するために、非常に簡単なロジックを使用します。ラインの傾斜とマーキングまでの距離に基づいて決定を行います。 メソッドは不完全ですが、単純な条件では機能しました



Lane



クラスは、左右のマークアップ行のコンテナです(リファクタリングが要求されます)。 このクラスは、マーキングラインの操作に関連するいくつかのメソッドも提供します。最も重要なメソッドはfit_lane_line



です。 新しいマーキングラインを作成するには、適切なマーキングセグメントをポイントとして表し、通常のnumpy.polyfit



関数を使用して1次多項式(つまりライン)でnumpy.polyfit



ます。



得られた道路標示線の安定化は非常に重要です。元の画像は非常にノイズが多く、車線の決定はフレームごとに行われます。 路面の影や不均一性は、マーキングの色をすぐに検出器が判断できない色に変更します...プロセスでは、いくつかの安定化方法を使用しました。



  1. バッファ 結果のマーキングラインは、以前のN個の状態を記憶し、現在のフレームのマーキングラインのステータスをバッファに順次追加します。
  2. バッファ内のデータに基づく追加の行フィルタリング。 変換とクリーニングの後、データのノイズを取り除くことができなかった場合、ラインが外れ値になる可能性があります。そして、線形モデルは外れ値に敏感です。 したがって、私たちにとって根本的に高い精度の価値は、完全性の重大な損失を損なうことさえあります。 簡単に言えば、モデルに外れ値を追加するよりも、正しい行を除外する方が適切です。 特にそのような場合のために、私はDECISION_MAT



    を作成しました。これは、現在のラインの傾きとバッファー内のすべてのラインの平均を相関させる方法を決定する「意思決定」マトリックスです。


たとえば、 DECISION_MAT = [ [ 0.1, 0.9] , [1, 0] ]



、2つのソリューションの選択を検討します。ラインを不安定(すなわち潜在的な外れ値)または安定(その勾配はバッファー内のこのストリップのラインの平均勾配に対応)プラス/マイナスのしきい値)。 線が不安定な場合でも、それを失いたくないのです。実際の道路の曲がり角に関する情報を伝えることができます。 小さい係数(この場合-0.1)を考慮して単純に考慮します安定したラインのために、以前のデータからの重みなしで現在のパラメーターを使用します。



現在のフレームのマーキングライン安定性インジケーターは、ブールクラスのLane



クラスのオブジェクトLane.right_lane.stable



およびLane.left_lane.stable



によって記述されます。 これらの変数の少なくとも1つがFalse



に設定されている場合、2本の線の間の赤いポリゴンとして視覚化します(以下のように見えます)。



その結果、かなり安定した行が得られます。











ステップ3:ソースイメージの描画と更新



線を正しく描画するために、地平線の座標を計算するかなり簡単なアルゴリズムを作成しました。これについては既に説明しました。 私のプロジェクトでは、この点は2つのことに必要です。



  1. マーキングラインの外挿をこのポイントに制限します。
  2. 地平線上のすべてのハフ線を除外します。


バンドを決定するプロセス全体を視覚化するために、小さなimage augmentation



を行いました。



 def draw_some_object(what_to_draw, background_image_to_draw_on, **kwargs): # do_stuff_and_return_image # Snapshot 1 out_snap1 = np.zeros_like(image) out_snap1 = draw_binary_mask(binary_mask, out_snap1) out_snap2 = draw_filtered_lines(segments, out_snap1) snapshot1 = cv2.resize(deepcopy(out_snap1), (240,135)) # Snapshot 2 out_snap2 = np.zeros_like(image) out_snap2 = draw_canny_edges(edges_mask, out_snap2) out_snap2 = draw_points(Lane.left_line.points, out_snap2, Lane.COLORS['left_line']) out_snap2 = draw_points(Lane.right_line.points, out_snap2, Lane.COLORS['right_line']) out_snap2 = draw_lane_polygon(out_snap2) snapshot2 = cv2.resize(deepcopy(out_snap2), (240,135)) # Augmented image output = deepcopy(image) output = draw_lane_lines([Lane.left_line, Lane.right_line], output, shade_background=True) output = draw_lane_polygon(output) output = draw_dashboard(output, snapshot1, snapshot2) return output
      
      









コードからわかるように、元のビデオに2つの画像を重ねます。1つはバイナリマスク、2つ目はすべてのフィルターを通過したハフ線(ポイントに変換)を使用しています。 元のビデオ自体に2つのレーンを設定します(前の画像のポイントに対する線形回帰)。 緑の長方形は、「不安定な」ラインの存在を示すインジケータです。ラインが存在すると、赤に変わります。 このアーキテクチャを使用すると、ダッシュボードとして表示されるフレームを簡単に変更および結合できるため、ソースコードを大幅に変更することなく、多くのコンポーネントとこれを同時に視覚化できます。











次は?



このプロジェクトはまだ完成にはほど遠いです。作業を重ねるほど、改善が必要なものが増えます。





プロジェクトのすべてのソースコードは、GitHubのリンクから入手できます。



PSそして今、私たちはすべてを壊します!



もちろん、この投稿にも楽しい部分が必要です。 方向と光が頻繁に変化する山道で、検出器がどれほど哀れになるかを見てみましょう。 最初はすべてが正常であるように見えますが、将来的には、バンドを決定する際のエラーが蓄積され、検出器はそれらを監視する時間がなくなります:









そして、光が非常に急速に変化する森林では、検出器はタスクを完全に失敗しました。









ところで、次のプロジェクトの1つは、非線形検出器を作成することです。これは、「フォレスト」タスクに対処するだけです。 新しい投稿をお楽しみに!



オリジナルの英語の投稿



All Articles