LCNN
概要
LCNNは、建築物や室内の画像から線分と接合点を検出し、構造的な線画(wireframe)を抽出することを目的とした深層学習モデルである。論文「End-to-End Wireframe Parsing」では、従来手法より高精度な線分検出と接合点検出を実現している。

主要機能
LCNNの主要機能は以下の通りである:

• 線分検出(Line Segment Detection) • 接合点検出(Junction Detection) • 構造化された線画解析(Wireframe Parsing)

論文情報
論文タイトル: End-to-End Wireframe Parsing(2019年)

著者: Yichao Zhou, Haozhi Qi, Yi Ma

発表: ICCV 2019

arXiv: https://arxiv.org/abs/1905.03246

技術的特徴
アーキテクチャ構成

LCNNは以下のモジュールで構成される:

• バックボーン: Stacked Hourglass Network • Junction Proposal Module (JPM): 接合点(交点)の検出 • Line Sample Module (LSM): 線分のサンプリング • Line Verification Module (LVM): 線分の検証

主要な技術的貢献

• 線分検出を端点ペアの組み合わせ問題として定式化 • 接合点検出と線分検出を統合的に学習 • 従来手法(LSD、Hough変換)より高精度な検出性能

GitHubリポジトリ
リポジトリURL: https://github.com/zhou13/lcnn

主要ファイル構成

• demo.py: 単一画像での推論デモ • process.py: 複数画像のバッチ処理 • eval-*.py: 評価スクリプト群 • config/wireframe.yaml: 学習設定ファイル • lcnn/models/: モデル実装

学習済みモデル

• HuggingFace Repoでの配布(312k iterationsで学習) • Wireframeデータセットで学習済み

実装手順
1. 環境構築

推論の基本的な流れ

画像の前処理(正規化、リサイズ)
モデルによる推論(junction, line maps)
後処理(NMS、線分の復元)
結果の可視化
カスタマイズのポイント

• config/wireframe.yamlで検出閾値の調整 • 出力される線分情報を消失点推定アルゴリズムに接続 • バッチ処理時はprocess.pyをベースに改造

消失点推定への応用
消失点推定との関係

LCNNの出力(高品質な線分)は消失点推定の入力として利用可能である。ただし、LCNN自体は消失点を直接推定しない。

応用フロー

LCNN: 画像 → 線分・接合点検出
後処理: 検出された線分 → 消失点推定(別途アルゴリズムが必要)
消失点推定の実装方法

LCNNで検出された線分情報から消失点を推定する方法:

• 線分のグループ化: 検出された線分を方向別にクラスタリング • RANSACによる消失点推定: 各グループ内で交点を計算 • Manhattan World仮定: 3つの主要な消失点を抽出

実装上の利点

• 高品質な線分検出により、ノイズの少ない消失点推定が可能 • 接合点情報を活用した構造的な制約の導入が可能

モデルロードの要件

process_with_lcnn関数で「LCNNモデルを正しくロード」するには、以下の要素が必要である:

• LCNNライブラリ自体のインストール • モデルアーキテクチャの正しい構築 • チェックポイントの適切なロード • 前処理・後処理の実装

評価
このモデルは建築物や室内シーンの構造解析において、従来手法より優れた性能を示しており、消失点推定タスクへの応用に適していると評価できる。







# LCNNによる線分検出と消失点推定体験

## 1. 概要

**主要技術名**
LCNN(Line Convolutional Neural Network)

**論文情報**
論文名称:End-to-End Wireframe Parsing
著者:Yichao Zhou, Haozhi Qi, Yi Ma
出典:ICCV 2019
arXiv:https://arxiv.org/abs/1905.03246

**技術の新規性・特徴と特徴を活かせるアプリ例**
LCNNは、建築物や室内の画像から線分と接合点を検出し、構造的な線画(wireframe)を抽出する深層学習モデルである。従来の線分検出手法(LSD、Hough変換)と異なり、線分検出を端点ペアの組み合わせ問題として定式化し、接合点検出と線分検出を統合的に学習する。これにより高精度な構造解析が可能となる。建築図面の自動解析、室内レイアウト認識、拡張現実における平面検出、ロボットナビゲーションにおける環境理解などに応用できる。

**技術を実際に実行して学ぶ体験価値**
リアルタイムWebカメラ映像から線分を検出し、消失点を推定する過程を通じて、深層学習による構造解析の実際の動作を体験できる。Manhattan World仮定やRANSACアルゴリズムの組み合わせによる頑健な推定手法を学習し、AIがどのように画像内の幾何学的構造を抽出し、空間の遠近感を自動的に認識するかを理解できる。

## 2. 事前準備

Python, Windsurfをインストールしていない場合の手順(インストール済みの場合は実行不要)。

1. 管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)。
2. 以下のコマンドをそれぞれ実行する(winget コマンドは1つずつ実行)。

```
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Windsurf をシステム領域にインストール
winget install --scope machine --id Codeium.Windsurf -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
REM Windsurf のパス設定
set "WINDSURF_PATH=C:\Program Files\Windsurf"
if exist "%WINDSURF_PATH%" (
    echo "%PATH%" | find /i "%WINDSURF_PATH%" >nul
    if errorlevel 1 setx PATH "%PATH%;%WINDSURF_PATH%" /M >nul
)
```

**LCNNリポジトリと依存パッケージのインストール**

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

```
winget install --scope machine --id Git.Git -e --silent
REM Git のパス設定
set "NEW_PATH=C:\Program Files\Git\cmd"
if exist "%NEW_PATH%" echo "%PATH%" | find /i "%NEW_PATH%" >nul
if exist "%NEW_PATH%" if errorlevel 1 setx PATH "%PATH%;%NEW_PATH%" /M >nul
```

新しいコマンドプロンプトを開き、以下を実行する。

```
pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install opencv-python matplotlib scipy scikit-image scikit-learn tensorboard tensorboardX huggingface-hub

# リポジトリをクローン
cd %USERPROFILE%\Documents
git clone https://github.com/zhou13/lcnn.git
```

**学習済みモデルの準備**
LCNNのGitHubリポジトリの指示に従い、学習済みモデル(190418-201834-f8934c6-lr4d10-312k.pth)をHuggingFaceからダウンロードし、`%USERPROFILE%\Documents\lcnn\pretrained\Pretrained\`フォルダに配置する。

## 3. プログラムコード

```python
# LCNN消失点検出プログラム
#   リアルタイムWebカメラ入力による線分検出と消失点推定
#   論文: "End-to-End Wireframe Parsing" (ICCV 2019)
#   GitHub: https://github.com/zhou13/lcnn
#   特徴: LCNNは線分と接合点を統合的に学習、従来手法より高精度な検出性能
#         建築物や室内シーンの構造解析に適用、消失点推定への応用が可能
#   前準備: pip install torch torchvision opencv-python scikit-image scikit-learn
#           cd %USERPROFILE%\Documents
#           git clone https://github.com/zhou13/lcnn.git

import os
import cv2
import numpy as np
import torch
import skimage.transform
import sys
import random

# 定数定義
DBSCAN_EPS = 150
DBSCAN_MIN_SAMPLES = 15
INTERSECTION_THRESHOLD = 1e-10
MIN_LINE_LENGTH = 50
ANGLE_THRESHOLD = 0.2
SCORE_THRESHOLD = 0.95
RANSAC_ITERATIONS = 1000
RANSAC_SAMPLE_SIZE = 2
RANSAC_DISTANCE_THRESHOLD = 5.0
MANHATTAN_ANGLE_TOLERANCE = 30.0
HISTOGRAM_BIN_WIDTH = 5.0
ITERATION_MAX_COUNT = 5
CONVERGENCE_THRESHOLD = 1.0
CLUSTER_RANGE = 15.0
ORTHOGONAL_TOLERANCE = 10.0
RANDOM_SEED = 42

# LCNNパスを追加
lcnn_path = os.path.join(os.path.expanduser('~'), 'Documents', 'lcnn')
sys.path.insert(0, lcnn_path)

# LCNNライブラリのインポート
import lcnn
from lcnn.config import C, M
from lcnn.models.line_vectorizer import LineVectorizer
from lcnn.models.multitask_learner import MultitaskHead, MultitaskLearner
from lcnn.postprocess import postprocess

# パス設定
config_path = os.path.join(lcnn_path, 'config', 'wireframe.yaml')
checkpoint_path = os.path.join(lcnn_path, 'pretrained', 'Pretrained', '190418-201834-f8934c6-lr4d10-312k.pth')

# デバイス設定
device = torch.device("cpu")

# LCNNモデルをロード
C.update(C.from_yaml(filename=config_path))
M.update(C.model)

checkpoint = torch.load(checkpoint_path, map_location=device)

model = lcnn.models.hg(
    depth=M.depth,
    head=lambda c_in, c_out: MultitaskHead(c_in, c_out),
    num_stacks=M.num_stacks,
    num_blocks=M.num_blocks,
    num_classes=sum(sum(M.head_size, [])),
)
model = MultitaskLearner(model)
model = LineVectorizer(model)
model.load_state_dict(checkpoint["model_state_dict"])
model = model.to(device)
model.eval()

# カメラ初期化(DirectShowバックエンド使用)
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

# メイン処理
while True:
    # バッファをクリア(最新フレームのみ取得)
    cap.grab()
    ret, frame = cap.retrieve()
    if not ret:
        break

    # フレームをLCNNで処理して線分を取得
    im = frame
    if im.ndim == 2:
        im = np.repeat(im[:, :, None], 3, 2)
    im = im[:, :, :3]

    # 512x512にリサイズ
    im_resized = skimage.transform.resize(im, (512, 512)) * 255
    image = (im_resized - M.image.mean) / M.image.stddev
    image = torch.from_numpy(np.rollaxis(image, 2)[None].copy()).float()

    # 推論実行
    with torch.no_grad():
        input_dict = {
            "image": image.to(device),
            "meta": [
                {
                    "junc": torch.zeros(1, 2).to(device),
                    "jtyp": torch.zeros(1, dtype=torch.uint8).to(device),
                    "Lpos": torch.zeros(2, 2, dtype=torch.uint8).to(device),
                    "Lneg": torch.zeros(2, 2, dtype=torch.uint8).to(device),
                }
            ],
            "target": {
                "jmap": torch.zeros([1, 1, 128, 128]).to(device),
                "joff": torch.zeros([1, 1, 2, 128, 128]).to(device),
            },
            "mode": "testing",
        }
        H = model(input_dict)["preds"]

    # 線分データを取得
    lines = H["lines"][0].cpu().numpy() / 128 * im.shape[:2]
    scores = H["score"][0].cpu().numpy()

    # 重複線分除去
    if len(lines) > 1:
        unique_lines = []
        unique_scores = []
        for i in range(len(lines)):
            is_duplicate = False
            for j in range(len(unique_lines)):
                if len(lines[i]) == len(unique_lines[j]) and np.allclose(lines[i], unique_lines[j], atol=1e-6):
                    is_duplicate = True
                    break
            if not is_duplicate:
                unique_lines.append(lines[i])
                unique_scores.append(scores[i])
        if len(unique_lines) > 0:
            lines = np.array(unique_lines)
            scores = np.array(unique_scores)
        else:
            lines = np.array([])
            scores = np.array([])

    # 後処理
    if len(lines) > 0:
        diag = (im.shape[0] ** 2 + im.shape[1] ** 2) ** 0.5
        nlines, nscores = postprocess(lines, scores, diag * 0.01, 0, False)
    else:
        nlines = []
        nscores = []

    # 閾値フィルタリング
    filtered_lines = []
    filtered_scores = []
    if len(nlines) > 0 and len(nscores) > 0:
        for line, score in zip(nlines, nscores):
            if score >= SCORE_THRESHOLD:
                filtered_lines.append(line)
                filtered_scores.append(score)

    # 線分から消失点を検出(改善されたアルゴリズム)
    vanishing_points = []
    if len(filtered_lines) >= 2:
        height, width = frame.shape[:2]

        # 線分の長さフィルタリング
        length_filtered_lines = []
        length_filtered_scores = []
        for line, score in zip(filtered_lines, filtered_scores):
            point1, point2 = line
            y1, x1 = point1
            y2, x2 = point2
            length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
            if length >= MIN_LINE_LENGTH:
                length_filtered_lines.append(line)
                length_filtered_scores.append(score)

        if len(length_filtered_lines) >= 2:
            # 段階1: 線分方向の事前クラスタリング(前処理)
            line_angles = []
            for line in length_filtered_lines:
                point1, point2 = line
                y1, x1 = point1
                y2, x2 = point2
                angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
                angle = angle % 180
                line_angles.append(angle)

            # 角度ヒストグラム作成
            num_bins = int(180 / HISTOGRAM_BIN_WIDTH)
            angle_histogram = np.zeros(num_bins)
            for angle in line_angles:
                bin_idx = int(angle / HISTOGRAM_BIN_WIDTH)
                if 0 <= bin_idx < num_bins:
                    angle_histogram[bin_idx] += 1

            # ピーク検出
            peak_threshold = max(1, len(length_filtered_lines) * 0.1)
            peak_angles = []
            for i in range(len(angle_histogram)):
                if angle_histogram[i] >= peak_threshold:
                    is_peak = True
                    if i > 0 and angle_histogram[i] < angle_histogram[i-1]:
                        is_peak = False
                    if i < len(angle_histogram)-1 and angle_histogram[i] < angle_histogram[i+1]:
                        is_peak = False
                    if is_peak:
                        peak_angles.append(i * HISTOGRAM_BIN_WIDTH)

            # クラスタ形成:各ピーク周辺の線分をグループ化
            direction_clusters = []
            for peak_angle in peak_angles:
                cluster_lines = []
                cluster_scores = []
                for i, angle in enumerate(line_angles):
                    angle_diff = min(abs(angle - peak_angle), 180 - abs(angle - peak_angle))
                    if angle_diff <= CLUSTER_RANGE:
                        cluster_lines.append(length_filtered_lines[i])
                        cluster_scores.append(length_filtered_scores[i])

                if len(cluster_lines) >= 2:
                    direction_clusters.append((cluster_lines, cluster_scores, peak_angle))

            # 段階2: Manhattan World制約の適用(制約条件)
            manhattan_groups = []

            if len(direction_clusters) >= 2:
                horizontal_clusters = []
                vertical_clusters = []

                for cluster_lines, cluster_scores, peak_angle in direction_clusters:
                    if (peak_angle <= MANHATTAN_ANGLE_TOLERANCE or
                        peak_angle >= (180 - MANHATTAN_ANGLE_TOLERANCE) or
                        abs(peak_angle - 90) <= MANHATTAN_ANGLE_TOLERANCE):
                        horizontal_clusters.append((cluster_lines, cluster_scores, peak_angle))
                    else:
                        vertical_clusters.append((cluster_lines, cluster_scores, peak_angle))

                horizontal_clusters.sort(key=lambda x: len(x[0]), reverse=True)
                manhattan_groups.extend(horizontal_clusters[:2])

                if vertical_clusters:
                    vertical_clusters.sort(key=lambda x: len(x[0]), reverse=True)
                    manhattan_groups.append(vertical_clusters[0])

            # フォールバック:クラスタが不十分な場合
            if len(manhattan_groups) < 2:
                horizontal_group1 = []
                horizontal_group2 = []
                vertical_group = []

                for i, angle in enumerate(line_angles):
                    line = length_filtered_lines[i]
                    score = length_filtered_scores[i]

                    if angle <= MANHATTAN_ANGLE_TOLERANCE or angle >= (180 - MANHATTAN_ANGLE_TOLERANCE):
                        horizontal_group1.append((line, score))
                    elif abs(angle - 90) <= MANHATTAN_ANGLE_TOLERANCE:
                        horizontal_group2.append((line, score))
                    else:
                        vertical_group.append((line, score))

                manhattan_groups = []
                if len(horizontal_group1) >= 2:
                    manhattan_groups.append((horizontal_group1, "horizontal1"))
                if len(horizontal_group2) >= 2:
                    manhattan_groups.append((horizontal_group2, "horizontal2"))
                if len(vertical_group) >= 2:
                    manhattan_groups.append((vertical_group, "vertical"))
            else:
                labeled_groups = []
                for i, (cluster_lines, cluster_scores, peak_angle) in enumerate(manhattan_groups):
                    group_data = [(line, score) for line, score in zip(cluster_lines, cluster_scores)]
                    if (peak_angle <= MANHATTAN_ANGLE_TOLERANCE or
                        peak_angle >= (180 - MANHATTAN_ANGLE_TOLERANCE)):
                        labeled_groups.append((group_data, "horizontal1"))
                    elif abs(peak_angle - 90) <= MANHATTAN_ANGLE_TOLERANCE:
                        labeled_groups.append((group_data, "horizontal2"))
                    else:
                        labeled_groups.append((group_data, "vertical"))
                manhattan_groups = labeled_groups

            # 段階3-5: RANSAC + 重み付き最小二乗法 + 反復改善
            estimated_vps = []

            for group_data, group_type in manhattan_groups:
                if len(group_data) >= 2:
                    group_lines = [item[0] for item in group_data]
                    group_scores = [item[1] for item in group_data]

                    # 乱数シードを設定
                    random.seed(RANDOM_SEED)

                    # RANSAC消失点推定
                    best_vp = None
                    best_support = 0

                    for iteration in range(RANSAC_ITERATIONS):
                        if len(group_lines) >= RANSAC_SAMPLE_SIZE:
                            sample_indices = random.sample(range(len(group_lines)), RANSAC_SAMPLE_SIZE)
                            line1 = group_lines[sample_indices[0]]
                            line2 = group_lines[sample_indices[1]]

                            # 交点計算
                            point1_1, point1_2 = line1
                            y1, x1 = point1_1
                            y2, x2 = point1_2

                            point2_1, point2_2 = line2
                            y3, x3 = point2_1
                            y4, x4 = point2_2

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

                            denom = v1[0] * v2[1] - v1[1] * v2[0]
                            if abs(denom) > INTERSECTION_THRESHOLD:
                                dx = x3 - x1
                                dy = y3 - y1
                                t = (dx * v2[1] - dy * v2[0]) / denom

                                vp_x = x1 + t * v1[0]
                                vp_y = y1 + t * v1[1]

                                # Manhattan World制約の適用
                                valid_vp = True
                                if group_type == "horizontal1" or group_type == "horizontal2":
                                    if not (-height <= vp_y <= 2*height):
                                        valid_vp = False
                                elif group_type == "vertical":
                                    if not (width*0.2 <= vp_x <= width*0.8):
                                        valid_vp = False

                                if valid_vp and -width <= vp_x <= 2*width and -height <= vp_y <= 2*height:
                                    # 支持線分数の評価
                                    support_count = 0
                                    for test_line in group_lines:
                                        test_point1, test_point2 = test_line
                                        test_y1, test_x1 = test_point1
                                        test_y2, test_x2 = test_point2

                                        line_length = np.sqrt((test_x2 - test_x1)**2 + (test_y2 - test_y1)**2)
                                        if line_length > 0:
                                            distance = abs((test_y2 - test_y1) * vp_x - (test_x2 - test_x1) * vp_y +
                                                         test_x2 * test_y1 - test_y2 * test_x1) / line_length

                                            if distance <= RANSAC_DISTANCE_THRESHOLD:
                                                support_count += 1

                                    if support_count > best_support:
                                        best_support = support_count
                                        best_vp = (vp_x, vp_y)

                    if best_vp is not None and best_support >= 2:
                        # 重み付き最小二乗法と反復改善
                        vp_x, vp_y = best_vp

                        for iteration in range(ITERATION_MAX_COUNT):
                            prev_vp_x, prev_vp_y = vp_x, vp_y

                            # 現在の消失点に基づいて距離を計算し、重みを設定
                            current_distances = []
                            for line in group_lines:
                                point1, point2 = line
                                y1, x1 = point1
                                y2, x2 = point2

                                line_length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
                                if line_length > 0:
                                    distance = abs((y2 - y1) * vp_x - (x2 - x1) * vp_y +
                                                 x2 * y1 - y2 * x1) / line_length
                                    current_distances.append(distance)
                                else:
                                    current_distances.append(float('inf'))

                            # 重み付き最小二乗法の行列形式での解法
                            A_matrix = []
                            b_vector = []
                            weights = []

                            for i, line in enumerate(group_lines):
                                point1, point2 = line
                                y1, x1 = point1
                                y2, x2 = point2

                                length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
                                confidence = group_scores[i]
                                distance = current_distances[i]

                                if length > 0 and distance < float('inf'):
                                    # 重み計算
                                    weight = length * confidence / (1 + distance * distance)
                                    weights.append(weight)

                                    # 直線の方程式 ax + by + c = 0
                                    a = y2 - y1
                                    b_coeff = x1 - x2
                                    c = x2 * y1 - x1 * y2

                                    # 正規化
                                    norm = np.sqrt(a*a + b_coeff*b_coeff)
                                    if norm > 0:
                                        a /= norm
                                        b_coeff /= norm
                                        c /= norm

                                        A_matrix.append([a, b_coeff])
                                        b_vector.append(-c)

                            if len(A_matrix) >= 2:
                                A_matrix = np.array(A_matrix)
                                b_vector = np.array(b_vector)
                                weights = np.array(weights)

                                # 重み付き最小二乗法
                                W = np.diag(weights)
                                AtWA = A_matrix.T @ W @ A_matrix
                                AtWb = A_matrix.T @ W @ b_vector

                                # 正則化項を追加
                                regularization = 1e-6 * np.eye(2)
                                AtWA += regularization

                                if np.linalg.det(AtWA) > 1e-10:
                                    solution = np.linalg.solve(AtWA, AtWb)
                                    new_vp_x, new_vp_y = solution

                                    # 収束判定
                                    change = np.sqrt((new_vp_x - vp_x)**2 + (new_vp_y - vp_y)**2)
                                    vp_x, vp_y = new_vp_x, new_vp_y

                                    if change < CONVERGENCE_THRESHOLD:
                                        break

                        estimated_vps.append((vp_x, vp_y, best_support, group_type))

            # 直交関係制約の適用(3つ以上の消失点がある場合のみ)
            if len(estimated_vps) >= 3:
                final_vps = []

                # 直交関係をチェック
                for i, (vp1_x, vp1_y, support1, type1) in enumerate(estimated_vps):
                    orthogonal_valid = True

                    for j, (vp2_x, vp2_y, support2, type2) in enumerate(estimated_vps):
                        if i != j:
                            # 2つの消失点から画像中心への方向ベクトル
                            center_x, center_y = width // 2, height // 2

                            vec1 = np.array([vp1_x - center_x, vp1_y - center_y])
                            vec2 = np.array([vp2_x - center_x, vp2_y - center_y])

                            # 内積を使って角度をチェック
                            if np.linalg.norm(vec1) > 0 and np.linalg.norm(vec2) > 0:
                                cos_angle = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
                                cos_angle = np.clip(cos_angle, -1, 1)
                                angle_deg = np.arccos(cos_angle) * 180 / np.pi

                                # 直交関係(90°±許容誤差)をチェック
                                if not (abs(angle_deg - 90) <= ORTHOGONAL_TOLERANCE):
                                    orthogonal_valid = False
                                    break

                    if orthogonal_valid and (-width*2 <= vp1_x <= width*3 and -height*2 <= vp1_y <= height*3):
                        final_vps.append((int(vp1_x), int(vp1_y), support1))

                vanishing_points = final_vps
            else:
                vanishing_points = [(int(vp[0]), int(vp[1]), vp[2]) for vp in estimated_vps]

    # 結果出力
    result = frame.copy()

    # 線分を描画(緑色)
    for line in filtered_lines:
        point1, point2 = line
        y1, x1 = point1
        y2, x2 = point2
        cv2.line(result, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)

    # 消失点を描画(赤色)
    for i, (vp_x, vp_y, count) in enumerate(vanishing_points):
        cv2.circle(result, (vp_x, vp_y), 8, (0, 0, 255), -1)
        cv2.putText(result, f'VP{i+1}', (vp_x + 10, vp_y - 10),
                   cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

    # 情報表示
    cv2.putText(result, f'Lines: {len(filtered_lines)}', (10, 30),
               cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
    cv2.putText(result, f'VP: {len(vanishing_points)}', (10, 60),
               cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)

    # 表示
    cv2.imshow('LCNN Vanishing Point Detection', result)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
```

## 4. 使用方法

**実行手順**

1. プログラムファイルを`lcnn_vanishing_point.py`として保存する。
2. Webカメラが接続されていることを確認する。
3. コマンドプロンプトで以下を実行する。

```
cd %USERPROFILE%\Documents
python lcnn_vanishing_point.py
```

4. Webカメラが起動し、リアルタイムで線分検出と消失点推定が実行される。
5. 緑色の線が検出された線分、赤色の円が推定された消失点を示す。
6. 'q'キーを押すとプログラムが終了する。

**動作確認のポイント**
建築物や室内の角、直線的な構造物をカメラに向けると、線分が検出され消失点が推定される。Manhattan World仮定に基づく3つの主要な消失点(水平方向2つ、垂直方向1つ)の検出が期待される。

## 5. 実験・探求のアイデア

**AIモデル選択による比較実験**
LCNN以外の線分検出手法(OpenCVのHoughLinesP、LSD)との比較実験を実施し、検出精度や処理速度の違いを評価する。config/wireframe.yaml内の設定変更による検出挙動の変化を観察する。

**実験要素の調整**
プログラム内の定数(SCORE_THRESHOLD、MIN_LINE_LENGTH、RANSAC_ITERATIONS等)を変更し、検出性能への影響を観察する。Manhattan World制約の有無による消失点推定精度の変化を検証する。

**体験・実験・探求のアイデア**
異なる環境(屋外建築物、室内空間、人工構造物)での検出性能の違いを調査する。カメラの角度や距離を変化させて、消失点推定の安定性を評価する。複数の消失点が同時に検出される条件を探求し、3点透視図法の原理を実際に確認する。検出された線分情報を用いて、建築物の3次元構造推定や平面検出への応用可能性を検討する。静止画入力への改造や処理速度の最適化についても実験し、LCNNの内部動作(Junction Proposal Module、Line Sample Module、Line Verification Module)の理解を深める。