少し前に、私はUdacityからプログラムを開始しました: “ Self-Driving Car Engineer Nanodegree” 。 オートパイロットで駆動システムを構築するさまざまな側面に関する多くのプロジェクトで構成されています。 最初のプロジェクト、つまり路面標示の単純な線形検出器に対する私の決定を提示します。 最後に何が起こったのかを理解するには、まずビデオを見てください:
このプロジェクトの目標は、レーンのレーンごとの認識のための単純な線形モデルを構築することです:入力でフレームを取得し、一連の変換を行います。これについては後で説明し、処理します。 プロジェクトは意図的にシンプルです。線形モデルのみ、良好な気象条件と視認性のみ、2本のマーキングラインのみです。 当然、これは実稼働ソリューションではありませんが、そのようなプロジェクトでもOpenCV、フィルターで十分に遊ぶことができ、一般に、自動車の自動操縦開発者が直面する困難を感じるのに役立ちます。
検出器の動作原理
検出器の構築プロセスは、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つの変換を適用します。
- Canny Border Detector :画像強度勾配を計算し、2つのしきい値を使用して弱い境界を削除し、目的の境界((
canny
)を使用)をcanny
関数のしきい値として使用する最適な境界検出アルゴリズム。 - ハフ変換: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
クラスのオブジェクトの主な目的は、この線が道路標示のセグメントであるかどうかを識別することです。 これを行うために、次のロジックにガイドされます。
- 線を水平にすることはできず、緩やかな勾配が必要です。
- 道路標示線と候補線の勾配の差が大きくなりすぎないようにしてください。
- 候補線は、それが属する道路標示から遠く離れてはなりません。
- 候補線は地平線の下にある必要があります
したがって、マーキングラインが属しているかどうかを判断するために、非常に簡単なロジックを使用します。ラインの傾斜とマーキングまでの距離に基づいて決定を行います。 メソッドは不完全ですが、単純な条件では機能しました
Lane
クラスは、左右のマークアップ行のコンテナです(リファクタリングが要求されます)。 このクラスは、マーキングラインの操作に関連するいくつかのメソッドも提供します。最も重要なメソッドは
fit_lane_line
です。 新しいマーキングラインを作成するには、適切なマーキングセグメントをポイントとして表し、通常の
numpy.polyfit
関数を使用して1次多項式(つまりライン)で
numpy.polyfit
ます。
得られた道路標示線の安定化は非常に重要です。元の画像は非常にノイズが多く、車線の決定はフレームごとに行われます。 路面の影や不均一性は、マーキングの色をすぐに検出器が判断できない色に変更します...プロセスでは、いくつかの安定化方法を使用しました。
- バッファ 結果のマーキングラインは、以前のN個の状態を記憶し、現在のフレームのマーキングラインのステータスをバッファに順次追加します。
- バッファ内のデータに基づく追加の行フィルタリング。 変換とクリーニングの後、データのノイズを取り除くことができなかった場合、ラインが外れ値になる可能性があります。そして、線形モデルは外れ値に敏感です。 したがって、私たちにとって根本的に高い精度の価値は、完全性の重大な損失を損なうことさえあります。 簡単に言えば、モデルに外れ値を追加するよりも、正しい行を除外する方が適切です。 特にそのような場合のために、私は
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つのことに必要です。
- マーキングラインの外挿をこのポイントに制限します。
- 地平線上のすべてのハフ線を除外します。
バンドを決定するプロセス全体を視覚化するために、小さな
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つは、非線形検出器を作成することです。これは、「フォレスト」タスクに対処するだけです。 新しい投稿をお楽しみに!
→ オリジナルの英語の投稿 。