LSD-VP消失点検出

概要

LSD(Line Segment Detector)は、画像から線分を検出するアルゴリズムである。画像の勾配情報を直接利用するため、Hough変換のような投票処理を必要とせず、線形時間で動作する。パラメータ調整を必要としない点も特徴である。消失点は、3次元空間で平行な線が2次元画像上で収束する点である。本教材のプログラムは、(1) LSDで画像から線分を検出、(2) 検出された線分の延長線の交点を計算、(3) HDBSCANで近接する交点をクラスタリング、(4) 各クラスタの中心を消失点として推定、という4段階で消失点を求める。

目次

関連する外部ページ

サイト内の関連情報

1. 概要

LSD(Line Segment Detector)は、画像から線分を検出するアルゴリズムである。

技術名:LSD(Line Segment Detector)
出典:Grompone von Gioi, R., Jakubowicz, J., Morel, J. M., & Randall, G. (2012). LSD: a Line Segment Detector. Image Processing On Line, 2, 35-55. https://doi.org/10.5201/ipol.2012.gjmr-lsd

この技術は、画像の勾配情報を直接利用して線分を検出するため、Hough変換のような投票処理が不要で、線形時間(画素数に比例する計算時間)で動作する。画像の局所的な勾配方向の一貫性を利用するため、閾値などのパラメータ調整が不要である。誤検出の数は、a contrario 検証(見かけの構造が偶然では生じにくいことを確かめる考え方)によって制御され、平均して1画像あたり1個に抑えられる。建築物の透視図法解析や道路認識に活用できる。

本教材では、カメラ映像から線分を検出し、HDBSCANで消失点を推定する。消失点は、3次元空間で平行な線が2次元画像上で収束する点であり、カメラの視点と被写体の幾何学的関係を表す。処理は、(1)LSDで画像から線分を検出、(2)検出された線分の延長線の交点を計算、(3)HDBSCANで近接する交点をクラスタリング、(4)各クラスタの中心を消失点として推定する、という4段階で行う。

HDBSCANは密度ベースクラスタリング手法で、ノイズに対する耐性があり、クラスタ数を事前に指定する必要がない。消失点検出では交点の数や分布が予測困難なため、HDBSCANが適している。

2. Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.html で解説しているので、必要に応じて参照してほしい。

3. Python 3.12 のインストール

Pythonのインストールを行い、Pythonのプログラムを実行する環境を整える。扱う環境は、Windows搭載パソコンである。金子研究室では、Python 3.12.10を推奨する。

[Windows での Python 3.12 のインストール手順を見るには、ここをクリック]

Windows での Python 3.12 のインストール

以下のいずれかの方法でPython 3.12をインストールする。Pythonがインストール済みの場合、この手順は不要である。

方法 1:winget によるインストール

インストールコマンドの実行方法

管理者権限コマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。

--scope machine を指定することで、システム全体(全ユーザー向け)にインストールされる。このオプションの実行には管理者権限が必要である。インストール完了後、コマンドプロンプトを再起動するとPATHが反映される。

REM Python 3.12 をシステム領域にインストール
winget install --id Python.Python.3.12 -e --scope machine --silent --accept-source-agreements --accept-package-agreements --override "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 Include_launcher=1 InstallLauncherAllUsers=1 TargetDir=\"C:\Program Files\Python312\""

REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; $c=[Environment]::GetEnvironmentVariable('Path','Machine'); if((Test-Path $p) -and (';'+$c+';' -notlike \"*;$p;*\") -and (';'+$c+';' -notlike \"*;$s;*\")){[Environment]::SetEnvironmentVariable('Path',\"$p;$s;$c\",'Machine')}"

方法 2:インストーラーによるインストール

  1. Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
  2. ダウンロードしたインストーラーを実行する。
  3. 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから python コマンドを実行できない。
  4. 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。

インストールの確認

コマンドプロンプトで以下を実行する。

python --version

バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。

4. Python の開発環境 Visual Studio Code のインストールと Python 用の設定

Python の開発環境Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を整える。

[Windows での Visual Studio Code のインストールと Python 用の設定手順を見るには、ここをクリック]

Windows での Visual Studio Code のインストールと Python 用の設定手順

1. VS Code と拡張機能のインストール

以下のコマンドにより,既存の VS Code を削除し,全ユーザー共有の設定で再インストールしたうえで,拡張機能(VS Code に機能を追加するソフトウェア)をまとめて導入する.

インストールコマンドの実行方法

管理者権限コマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして,コマンド全体をコマンドプロンプトにコピー&ペーストする。

インストールコマンド


REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
winget uninstall -e --id Microsoft.VisualStudioCode --silent --disable-interactivity --accept-source-agreements
rmdir /s /q C:\ProgramData\vscode-extensions 2>nul
rmdir /s /q "%APPDATA%\Code" 2>nul
rmdir /s /q "%USERPROFILE%\.vscode" 2>nul
rmdir /s /q "%LOCALAPPDATA%\Microsoft\vscode-update" 2>nul

REM VS Code をシステム領域に新規インストール
winget install --scope machine --id Microsoft.VisualStudioCode -e --silent --accept-source-agreements --accept-package-agreements

REM 全ユーザー共有の拡張機能フォルダ
mkdir C:\ProgramData\vscode-extensions 2>nul
icacls "C:\ProgramData\vscode-extensions" /grant "Everyone:(OI)(CI)M" /T

REM スタートメニューのショートカットを --extensions-dir 付きで再作成
rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" 2>nul
del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" 2>nul
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); $lnk.TargetPath='C:\Program Files\Microsoft VS Code\Code.exe'; $lnk.Arguments='--extensions-dir \"C:\ProgramData\vscode-extensions\"'; $lnk.Save()"
REM ショートカットの検証
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); Write-Host 'TargetPath:' $lnk.TargetPath; Write-Host 'Arguments:' $lnk.Arguments"

REM ファイル / フォルダ右クリックの「Code で開く」を登録
reg add "HKLM\SOFTWARE\Classes\*\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\Background\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%V\"" /f

REM --extensions-dir 付きで起動する code.cmd ラッパを作成
REM (%* を echo で書くと対話的 cmd で失われるため、PowerShell で [char]37+'*' を書き出す)
powershell -NoProfile -Command "$pct=[char]37; $q=[char]34; $c='@echo off'+[char]13+[char]10+$q+'C:\Program Files\Microsoft VS Code\bin\code.cmd'+$q+' --extensions-dir '+$q+'C:\ProgramData\vscode-extensions'+$q+' '+$pct+'*'+[char]13+[char]10; [IO.File]::WriteAllText('C:\ProgramData\vscode-extensions\vscode.cmd',$c,[Text.Encoding]::ASCII)"

REM 拡張機能のインストール
set "CODE=C:\Program Files\Microsoft VS Code\bin\code.cmd"
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot-chat
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.python
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.vscode-pylance
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.debugpy
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension MS-CEINTL.vscode-language-pack-ja
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension saoudrizwan.claude-dev
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension rust-lang.rust-analyzer
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension tamasfe.even-better-toml
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension anthropic.claude-code
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension almenon.arepl
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --list-extensions --show-versions
echo === セットアップ完了 ===

2. Python インタプリタの選択

同一マシンに複数の Python がインストールされている場合,VS Code で使用する Python 本体(インタプリタ:Python プログラムを解釈・実行するソフトウェア)を選択する必要がある.

  1. コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(Ctrl+Shift+P
  2. Python: Select Interpreter と入力する
  3. 表示される一覧から,使用する Python(例:C:\Program Files\Python312\python.exe)を選択する.

5. LSD-VP消失点検出プログラム

必要なライブラリのインストール

管理者権限コマンドプロンプトを起動する (手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。

次を実行する。--no-user は、ユーザ領域ではなくシステム領域へインストールするオプションである。

set "PYTHON_PATH=C:\Program Files\Python312"
"%PYTHON_PATH%\Scripts\pip" install --no-user -U opencv-python numpy scikit-learn

HDBSCAN は scikit-learnsklearn.cluster.HDBSCAN として同梱されているため、別途のクラスタリング用パッケージは不要である。日本語の画面表示には OpenCV 5 の cv2.FontFace("uni") を使うため、フォントファイルの用意も不要である。

概要

このプログラムは、動画像から線分を検出し、クラスタリング手法を用いて消失点を推定する。透視図法による空間認識を模倣し、建築物や道路などの人工環境における構造を把握する。

主要技術

参考文献

ソースコード

# LSD-VP消失点検出プログラム
#   特徴技術名: LSD (Line Segment Detector)
#   出典: Rafael Grompone von Gioi, Jérémie Jakubowicz, Jean-Michel Morel, and Gregory Randall, "LSD: a Line Segment Detector," Image Processing On Line, 2 (2012), pp. 35-55. https://doi.org/10.5201/ipol.2012.gjmr-lsd
#   特徴機能: パラメータ調整不要での自動線分検出 - 任意のデジタル画像に対してパラメータ調整なしで動作し、線形時間でサブピクセル精度の線分検出を実現する機能。a contrario 検証(Helmholtz原理に基づく)による誤検出制御により、平均して1画像につき1つの誤検出に制限。
#   学習済みモデル: 使用していない
#   方式設計:
#     関連利用技術:
#       - HDBSCAN: 階層密度ベースクラスタリング(Campello et al., 2013)- 異なる密度のクラスタを検出可能で、線分交点から消失点を自動検出
#       - OpenCV: コンピュータビジョンライブラリ - 画像処理、カメラ入力処理、日本語テキスト描画
#       - NumPy: 数値計算ライブラリ - 行列演算と幾何計算
#       - scikit-learn: HDBSCAN クラスタリング実装(sklearn.cluster.HDBSCAN)
#     入力と出力:
#       - 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.aviを使用)
#       - 出力: 処理結果が画像化できる場合にはOpenCV画面でリアルタイムに表示.OpenCV画面内に処理結果をテキストで表示.さらに,各フレームごとに,print()で処理結果を表示.プログラム終了時にprint()で表示した処理結果をresult.txtファイルに保存し,「result.txtに保存」したことをprint()で表示.プログラム開始時に,プログラムの概要,ユーザが行う必要がある操作(もしあれば)をprint()で表示.
#     処理手順:
#       1. LSDアルゴリズムによる線分検出
#       2. 線分の延長線の交点計算(画像外の交点も含む)
#       3. HDBSCANクラスタリングによる消失点推定
#       4. 結果の可視化
#     前処理: グレースケール変換による勾配計算最適化
#     後処理: 短い線分除去による精度向上
#     追加処理: 平行線分除去(角度差閾値による判定)- 消失点推定精度向上
#     調整を必要とする設定値:
#       - HDBSCAN_MIN_CLUSTER_SIZE(最小クラスタサイズ): 消失点検出精度を左右するパラメータ
#   将来方策: HDBSCAN_MIN_CLUSTER_SIZEを画像解像度と検出された線分数に基づいて動的に調整する機能の実装。具体的には、画像面積と線分数の比率から最適なクラスタサイズを自動計算する
#   その他の重要事項: 建築物や道路の透視図法解析に特化、画像外の消失点も検出
#   前準備: pip install --no-user -U opencv-python numpy scikit-learn

import cv2
import numpy as np
from sklearn.cluster import HDBSCAN
import tkinter as tk
from tkinter import filedialog
import urllib.request
import time
from datetime import datetime
import sys

# --- 設定 ---
BASE_HDBSCAN_MIN_CLUSTER_SIZE = 15  # ベースの最小クラスタサイズ(動的調整の基準)
HDBSCAN_MIN_SAMPLES = 5
INTERSECTION_THRESHOLD = 1e-10
VP_RADIUS = 15
MIN_LINE_LENGTH = 50
RANDOM_SEED = 42
# デフォルト角度閾値(ラジアン)
DEFAULT_ANGLE_THRESHOLD = 0.2
# デフォルト:消失点許容距離の倍率(画像対角長に対する倍率)
DEFAULT_MAX_VP_MULTIPLIER = 2.0
# 各消失点の最小支持線分数(ユニーク線分数の下限)
SUPPORT_MIN_LINES = 4
# 日本語テキスト描画(OpenCV 5 の FontFace。外部フォントファイルを必要としない)
FONT = cv2.FontFace("uni")
FONT_SIZE = 20
TEXT_COLOR = (255, 255, 255)
VP_COLORS = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]

np.random.seed(RANDOM_SEED)

frame_count = 0
results_log = []


# 動的に HDBSCAN の min_cluster_size を計算する関数
def compute_dynamic_min_cluster_size(w, h, num_lines, base=BASE_HDBSCAN_MIN_CLUSTER_SIZE):
    # 基本方針: 画像面積と検出線分数に応じて min_cluster_size をスケールさせる
    # 参考の基準解像度は 1280x720
    ref_diag = np.sqrt(1280.0**2 + 720.0**2)
    diag = np.sqrt(w**2 + h**2)
    diag_scale = diag / ref_diag
    # 線分数の影響(少ないと小さめ、多いと大きめ)
    line_scale = max(1.0, num_lines / 50.0)
    return int(max(3, base * diag_scale * line_scale))


# ユーザ設定の取得(角度閾値と距離倍率)
def ask_user_settings():
    inp = input(f"角度閾値(ラジアン, デフォルト {DEFAULT_ANGLE_THRESHOLD})を入力(Enterでデフォルト): ")
    angle_thresh = float(inp) if inp.strip() != "" else DEFAULT_ANGLE_THRESHOLD

    inp = input(f"消失点許容距離倍率(画像対角長の倍率, デフォルト {DEFAULT_MAX_VP_MULTIPLIER})を入力(Enterでデフォルト): ")
    max_vp_multiplier = float(inp) if inp.strip() != "" else DEFAULT_MAX_VP_MULTIPLIER

    return angle_thresh, max_vp_multiplier


# 初期値(開始メッセージ表示後にユーザ入力で上書き)
ANGLE_THRESHOLD = DEFAULT_ANGLE_THRESHOLD
MAX_VP_MULTIPLIER = DEFAULT_MAX_VP_MULTIPLIER


# フレーム処理
def video_frame_processing(frame):
    global frame_count
    current_time = time.time()
    frame_count += 1

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    lsd = cv2.createLineSegmentDetector(0)
    lines = lsd.detect(gray)[0]

    vanishing_points = []
    vp_in_image = []
    vp_in_image_supports = []  # 画像内VPに対応する支持線分集合

    filtered_lines = []

    if lines is not None and len(lines) >= 2:
        # 短い線分を除外
        for line in lines:
            x1, y1, x2, y2 = line
            length = np.hypot(x2 - x1, y2 - y1)
            if length >= MIN_LINE_LENGTH:
                filtered_lines.append(line)

        if len(filtered_lines) >= 2:
            h, w = frame.shape[:2]
            intersections = []
            intersection_pairs = []  # 交点を生成した線分ペア (i, j)

            for i in range(len(filtered_lines)):
                for j in range(i + 1, len(filtered_lines)):
                    x1, y1, x2, y2 = filtered_lines[i]
                    x3, y3, x4, y4 = filtered_lines[j]

                    v1 = np.array([x2 - x1, y2 - y1])
                    v2 = np.array([x4 - x3, y4 - y3])

                    angle1 = np.arctan2(v1[1], v1[0])
                    angle2 = np.arctan2(v2[1], v2[0])
                    angle_diff = abs(angle1 - angle2)
                    if angle_diff > np.pi:
                        angle_diff = 2 * np.pi - angle_diff

                    # 角度差が小さい(ほぼ平行)な場合は交点をスキップ
                    if angle_diff < ANGLE_THRESHOLD:
                        continue

                    denom = v1[0] * v2[1] - v1[1] * v2[0]
                    if abs(denom) < INTERSECTION_THRESHOLD:
                        continue

                    dx = x3 - x1
                    dy = y3 - y1
                    t = (dx * v2[1] - dy * v2[0]) / denom

                    x = x1 + t * v1[0]
                    y = y1 + t * v1[1]

                    center_x, center_y = w / 2, h / 2
                    distance = np.hypot(x - center_x, y - center_y)

                    # 画像対角長ベースで最大距離を計算
                    diag = np.hypot(w, h)
                    max_distance = MAX_VP_MULTIPLIER * diag

                    if distance < max_distance and np.isfinite(x) and np.isfinite(y):
                        intersections.append((x, y))
                        intersection_pairs.append((i, j))

            # HDBSCAN を実行(対応付け+ロバスト中心)
            if len(intersections) >= 2:
                points = np.array(intersections)

                # 動的に min_cluster_size を計算し、点数に応じて安全にクリップ
                dynamic_min_cluster = compute_dynamic_min_cluster_size(w, h, len(filtered_lines))
                n_points = len(points)
                min_cluster_size = max(2, min(dynamic_min_cluster, n_points))
                min_samples_val = max(1, min(HDBSCAN_MIN_SAMPLES, n_points - 1))

                clustering = HDBSCAN(min_cluster_size=min_cluster_size,
                                     min_samples=min_samples_val, copy=False).fit(points)

                # ラベル列挙順を安定化(-1 はノイズとして除外)
                unique_labels = sorted(set(clustering.labels_) - {-1})

                for label in unique_labels:
                    idxs = np.where(clustering.labels_ == label)[0]
                    if len(idxs) == 0:
                        continue

                    cluster_points = points[idxs]

                    # クラスタの支持線分集合を構築(ユニーク線分数)
                    support_lines = set()
                    for idx in idxs:
                        li, lj = intersection_pairs[idx]
                        support_lines.add(li)
                        support_lines.add(lj)

                    # 最小支持線分数の下限でフィルタ
                    if len(support_lines) < SUPPORT_MIN_LINES:
                        continue

                    # メドイド(総距離最小の点)を中心として採用
                    m = len(cluster_points)
                    if m == 1:
                        vp = cluster_points[0]
                    else:
                        sums = []
                        for k in range(m):
                            diffs = cluster_points - cluster_points[k]
                            dists = np.hypot(diffs[:, 0], diffs[:, 1])
                            sums.append(np.sum(dists))
                        medoid_idx = int(np.argmin(sums))
                        vp = cluster_points[medoid_idx]

                    vanishing_points.append(vp)

                    if 0 <= vp[0] <= w and 0 <= vp[1] <= h:
                        vp_in_image.append(vp)
                        vp_in_image_supports.append(support_lines)

    # 結果画像作成
    result = frame.copy()

    # すべての検出線分を薄く描画(緑)
    if lines is not None:
        for line in lines:
            x1, y1, x2, y2 = map(int, line)
            cv2.line(result, (x1, y1), (x2, y2), (0, 200, 0), 1)

    # フィルタ後の線分を強調表示(薄青)
    for line in filtered_lines:
        x1, y1, x2, y2 = map(int, line)
        cv2.line(result, (x1, y1), (x2, y2), (200, 200, 0), 2)

    # 消失点を描画(円とラベル)
    for i, vp in enumerate(vp_in_image[:3]):
        x, y = int(vp[0]), int(vp[1])
        cv2.circle(result, (x, y), VP_RADIUS, VP_COLORS[i % len(VP_COLORS)], -1)
        cv2.putText(result, f"消失点{i+1}", (x + 20, y - 10), TEXT_COLOR, FONT, FONT_SIZE)

    # 消失点から支持線分の中点へ線を描画(対応付け済みの線分のみ)
    for i, vp in enumerate(vp_in_image[:3]):
        color = VP_COLORS[i % len(VP_COLORS)]
        vx, vy = vp
        support_set = vp_in_image_supports[i] if i < len(vp_in_image_supports) else set()
        for line_idx in support_set:
            x1, y1, x2, y2 = filtered_lines[line_idx]
            mx, my = (x1 + x2) / 2.0, (y1 + y2) / 2.0
            cv2.line(result, (int(vx), int(vy)), (int(mx), int(my)), color, 1)

    # 日本語テキスト表示(全検出 / 使用)
    total_lines = len(lines) if lines is not None else 0
    used_lines = len(filtered_lines)
    cv2.putText(result, f"線分数: {total_lines}(使用: {used_lines}), 消失点: {len(vanishing_points)}(画像内: {len(vp_in_image)})",
                (10, 30), TEXT_COLOR, FONT, FONT_SIZE)

    # 結果文字列(座標と支持線分数は vp_in_image と対応)
    result_str = f"消失点数: {len(vanishing_points)}個(画像内: {len(vp_in_image)}個), 線分数: {total_lines} (使用: {used_lines})"
    if vp_in_image:
        result_str += " 座標:"
        limit = min(3, len(vp_in_image))
        for i in range(limit):
            vp = vp_in_image[i]
            support_count = len(vp_in_image_supports[i]) if i < len(vp_in_image_supports) else 0
            result_str += f" 消失点{i+1}({vp[0]:.0f},{vp[1]:.0f},支持:{support_count})"

    return result, result_str, current_time


# --- 実行開始 ---
print("=== LSD-VP消失点検出プログラム ===")
print("概要: 動画/カメラ映像から線分を検出し、線分の交点をHDBSCANでまとめて消失点を推定する。")
print("操作方法: q キー: プログラム終了")

# ユーザ設定の取得は開始メッセージ後に実施
ANGLE_THRESHOLD, MAX_VP_MULTIPLIER = ask_user_settings()

print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")

choice = input("選択: ")

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    if not path:
        print('ファイルが選択されませんでした。終了します。')
        sys.exit(0)
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
else:
    SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
    SAMPLE_FILE = 'vtest.avi'
    urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
    cap = cv2.VideoCapture(SAMPLE_FILE)

if not cap.isOpened():
    print('動画ファイル・カメラを開けませんでした')
    sys.exit(1)

print('\n=== 動画処理開始 ===')
print('操作方法: q キー: プログラム終了')
try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        MAIN_FUNC_DESC = "LSD-VP消失点検出"
        processed_frame, result, current_time = video_frame_processing(frame)
        cv2.imshow(MAIN_FUNC_DESC, processed_frame)

        if choice == '1':
            prefix = datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
        else:
            prefix = str(frame_count)
        log_line = f"{prefix} {result}"
        print(log_line)
        results_log.append(log_line)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
finally:
    print('\n=== プログラム終了 ===')
    cap.release()
    cv2.destroyAllWindows()
    if results_log:
        with open('result.txt', 'w', encoding='utf-8') as f:
            f.write('=== 結果 ===\n')
            f.write(f'処理フレーム数: {frame_count}\n')
            f.write('\n')
            f.write('\n'.join(results_log))
        print('\n処理結果をresult.txtに保存しました')

6. 使用方法

  1. プログラムを実行する
  2. 角度閾値と消失点許容距離倍率を入力する(Enterのみでデフォルト値)
  3. 入力を「0:動画ファイル,1:カメラ,2:サンプル動画」から選ぶ
  4. カメラを選んだ場合は、カメラを建築物、廊下、道路などに向ける
  5. 検出されたすべての線分が細い緑色、消失点の推定に使った線分が太い水色で表示される
  6. 画像内に収まった消失点が、赤・緑・青の円と「消失点1」などのラベルで表示される(画像外の消失点は画面に描画されず、個数だけが表示される)
  7. 'q'キーを押すとプログラムが終了し、各フレームの結果が result.txt に保存される

7. 実験・探求のアイデア

パラメータ調整による検出精度の変化

主要パラメータの役割:BASE_HDBSCAN_MIN_CLUSTER_SIZEはクラスタとして認める交点数の目安を決め、MIN_LINE_LENGTHはノイズとなる短い線分を除外し、ANGLE_THRESHOLDは平行に近い線分の交点を除外する。

環境での消失点検出実験

アルゴリズムの比較実験

応用実験