電算倶楽部 富山県のコンピュータ社会人サークル

富山県、特に滑川市、富山市、魚津市周辺で活動している社会人サークルです。

カメラの画像から線検出

概要

カメラで撮った画像から直線状のものが正しい位置にあるかどうかを判定したく、 まずPythonで直線を検出するプログラムを作成しました。

これに加え、線が目的の位置にあるかを判定する機能を作れば、目的を果たせそうです。

使用する検出法により特色があります。以下4つを切り替えられるようにしました。

  • ハフ変換
  • 確率的ハフ変換
  • LSD
  • FLD

実行イメージ

ウェブカメラで撮影した画像に、検知した直線を赤線で描画します。

f:id:s-densan:20200916212451p:plain
実行イメージ

操作方法

  • z: 次の検出方法に変更
  • a: 検出する線の最小長を増やす(検出条件を厳しくする)
  • s: 検出する線の最小長を減らす(検出条件をゆるくする)
  • ESC: 終了する

作成方法

必要ライブラリ

pipを使ったインストールコマンドです。

pip install opencv-contrib-python
pip install pylsd-nova
pip install numpy

画面描画やハフ変換・確率的ハフ変換・FLDを使用するためにOpenCVを導入します。opencv-contrib-pythonではFLDでエラーが出たので、extraが含まれるopencv-contrib-pythonとします。 LSD用にPyLSDを導入します。pylsdだとうまく行かない?よくわかりませんがフォークの一つであるpylsd-novaならうまくいきました。

ソース

from cv2 import cv2
import math
import numpy as np
from pylsd.lsd import lsd

cap = cv2.VideoCapture(0)
fgbg = cv2.createBackgroundSubtractorMOG2()

# 確率的ハフ変換
def detect_lines_hough_p(img):
    
    # グレイスケールに変換
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 線検出
    edges = cv2.Canny(gray, 50, 150, apertureSize=3)
    lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/360, threshold=50, minLineLength=50, maxLineGap=10)

    return  [l[0] for l in lines]


# ハフ変換
def detect_lines_hough(img):
    ret_lines = []
    
    # グレイスケールに変換
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    canny = cv2.Canny(gray, 50, 150, apertureSize=3)
    # 線検出
    lines = cv2.HoughLines(canny, 1, np.pi/180, 200)
    if lines is not None:
        for line in lines:
            for rho, theta in line:
                
                x1 = np.cos(theta)*rho - 1000*np.sin(theta)
                y1 = np.sin(theta)*rho + 1000*np.cos(theta)
                x2 = np.cos(theta)*rho + 1000*np.sin(theta)
                y2 = np.sin(theta)*rho - 1000*np.cos(theta)

                ret_lines.append((x1, y1, x2, y2))
        return ret_lines
    else:
        return []

# LSD
# use pylsd-nova
def detect_lines_lsd(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    linesL = lsd(gray)
    lines = [(x1, y1, x2, y2) for (x1, y1, x2, y2, width) in linesL]
    return lines


# FLD
def detect_lines_fld(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # FLDインスタンス生成
    length_threshold = 4 # 10
    distance_threshold = 1.41421356
    canny_th1 = 50.0
    canny_th2 = 50.0
    canny_aperture_size = 3
    do_merge = False

    # 線検出
    fld = cv2.ximgproc.createFastLineDetector(length_threshold,distance_threshold,
                    canny_th1,canny_th2,canny_aperture_size,do_merge)
    # ライン取得
    lines = fld.detect(gray)
    return  [l[0] for l in lines]


def main():
    # 線検出タイプ
    detect_lines_types = [
        ('FLD', detect_lines_fld ),
        ('LSD', detect_lines_lsd ),
        ('HoughLines', detect_lines_hough),
        ('HoughLinesP', detect_lines_hough_p),
    ]

    # 選択中線検出タイプ番号
    selection = 0

    width = 640
    height = 480
    # 線マスクの色
    mask_color = (0, 0, 255)
    # 線の最小長さ
    min_len = 000

    while True:
        ret, frame = cap.read()
        img = cv2.resize(frame, (width, height))
        
        if detect_lines_types[selection][1] is not None:
            lines = detect_lines_types[selection][1](frame)
            # 255で埋まった3次元配列(縦x横xRGB3次元)を生成
            mask = np.full((height, width, 3), 255, np.uint8)
            # 線の数
            cnt = 0
            for line in lines:
                x1, y1, x2, y2 = line
                # マスクに赤線を引く
                if (x1 - x2) ** 2 + (y1 - y2) ** 2 >= min_len ** 2:
                    mask = cv2.line(mask, (int(x1), int(y1)), (int(x2), int(y2)), mask_color, 3)
                    cnt += 1
            # マスクかけ
            masked_img = cv2.bitwise_and(img, mask)

            # 文字描画
            black = (0, 0, 0)
            white = (255, 255, 255)
            text1 = f'{selection+1}/{len(detect_lines_types)} {detect_lines_types[selection][0]}'
            img_with_text = cv2.putText(masked_img, text1, (1, 31), 0, 1, black)
            img_with_text = cv2.putText(masked_img, text1, (0, 30), 0, 1, white)
            img_with_text = cv2.putText(masked_img, f'line num: {cnt}, min length: {min_len}', (1, 61), 0, 1, black)
            img_with_text = cv2.putText(masked_img, f'line num: {cnt}, min length: {min_len}', (0, 60), 0, 1, white)
            img_with_text = cv2.putText(masked_img, f'z: next detection type, a: inc min line length, s:dec min line length, ESC: exit', (1, height - 20 + 1), 0, 0.5, black)
            img_with_text = cv2.putText(masked_img, f'z: next detection type, a: inc min line length, s:dec min line length, ESC: exit', (0, height - 20), 0, 0.5, white)
            cv2.imshow('masked', img_with_text)

        k = cv2.waitKey(1)
        if k == 27:
            break
        elif k == ord('z'):
            selection = (selection + 1) % len(detect_lines_types)
        elif k == ord('a'):
            min_len += 20
        elif k == ord('s'):
            min_len = max(min_len - 20, 0)

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

簡単な解説

関数detect_*にカメラで撮影した画像を渡すと、検出した線の情報を(x1, y1, x2, y2)のタプルのリストとして返却します。

確率的ハフ変換、LSD、FLDは線分として検出し、ハフ変換は直線として検出するようです。

線分で検出するものは短い線も検出されてしまうので、ある程度の長さ以上でフィルタを掛けられるようmin_lenを用意しています。

終わりに

検出自体はほとんどライブラリ機能を呼び出しているだけなので、シンプルに実装できました。 ただしこのままだと過剰な検出がされているので、この情報から不要な情報の除外やパラメタ調整などをして使える情報に加工することが必要そうです。