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)の理解を深める。