LSD とカラー画像の勾配情報による直線検出(ソースコードと実行結果)


Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -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
【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine Codeium.Windsurf -e --silent
【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
Visual Studio 2022 Build Toolsとランタイムのインストール
pytlsd のインストールに使用する.
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Visual Studio 2022 Build Toolsとランタイムのインストール
winget install --scope machine Microsoft.VisualStudio.2022.BuildTools Microsoft.VCRedist.2015+.x64
set VS_INSTALLER="C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe"
set VS_PATH="C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools"
REM C++開発ワークロードのインストール
%VS_INSTALLER% modify --installPath %VS_PATH% ^
--add Microsoft.VisualStudio.Workload.VCTools ^
--add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ^
--add Microsoft.VisualStudio.Component.Windows11SDK.22621 ^
--includeRecommended --quiet --norestart
必要なライブラリのインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する
pip install opencv-python numpy pillow
pytlsd使用時の追加準備:
pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install pytlsd
LSD とカラー画像の勾配情報による直線検出プログラム
概要
このプログラムは、動画の各フレームから直線セグメントを自動的に検出する。カラー画像の勾配情報とグレースケール変換を組み合わせた前処理により、エッジ特徴を強調し、LSDアルゴリズムで直線検出を実現する。
主要技術
Line Segment Detector (LSD)
Rafael Grompone von Gioiらが2010年に開発した直線検出アルゴリズム[1]。画像の勾配情報に基づいて線分を検出し、誤検出制御機構を備える。確率的ハフ変換と比較して、サブピクセル精度での検出が可能で、パラメータ調整が少ない。OpenCV 4.6以降に標準実装されている。
Sobel演算子による勾配計算
画像の微分近似により、エッジの強度と方向を計算する画像処理技術[2]。本プログラムではRGB各チャンネルに適用し、カラー情報を活用したエッジ検出を実現している。
技術的特徴
カラー画像の勾配情報活用
RGB各チャンネルでSobel演算子を適用し、統合勾配マップを生成する。
適応的パラメータ調整
画像の解像度と明度に応じてLSDパラメータを動的に調整する。
多段階前処理
ガウシアンフィルタ、コントラスト強化、CLAHE(適応的ヒストグラム均等化)を組み合わせる。
重み付き画像統合
グレースケール画像と勾配情報を7:3の比率で統合する。
実装の特色
動画のリアルタイム処理に対応し、以下の機能を備える:
- 3つの入力ソース選択(ファイル、カメラ、サンプル動画)
- 線分長フィルタリング(30ピクセル以上)
- 左右分割表示(入力画像と検出結果)
- 処理結果のファイル保存機能
参考文献
[1] Grompone von Gioi, R., Jakubowicz, J., Morel, J. M., & Randall, G. (2010). LSD: A Fast Line Segment Detector with a False Detection Control. IEEE Transactions on Pattern Analysis and Machine Intelligence, 32(4), 722-732. https://doi.org/10.1109/TPAMI.2008.300
[2] Sobel, I., & Feldman, G. (1968). A 3x3 Isotropic Gradient Operator for Image Processing. Stanford Artificial Intelligence Project (SAIL).
ソースコード
# LSD とカラー画像の勾配情報による直線検出プログラム
# 特徴技術名: Line Segment Detector (LSD)
# 出典: Rafael Grompone von Gioi, Jeremie Jakubowicz, Jean-Michel Morel, and Gregory Randall. LSD: A Fast Line Segment Detector with a False Detection Control. IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 32, no. 4, pp. 722-732, 2010.
# 特徴機能: Rafael Grompone von Gioiらが開発したオリジナルのLSDアルゴリズムによる直線検出。グレースケール画像から直接線分を検出し、確率的ハフ変換より高精度でサブピクセル精度を実現。OpenCVの実装とpytlsdの実装を選択可能
# 学習済みモデル: 使用なし
# 方式設計:
# 関連利用技術:
# - OpenCV 4.6以上: コンピュータビジョンライブラリ、Rafael Grompone von GioiのオリジナルLSDアルゴリズム実装
# - pytlsd: LSDアルゴリズムの代替実装(オプション)
# - PyTorch: GPU/CPU自動選択(pytlsd使用時)
# - NumPy: 数値計算ライブラリ、配列操作とパラメータ計算
# 入力と出力:
# 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi を使用)
# 出力: リアルタイムOpenCV画面表示(左:LSD入力画像、右:検出結果)、処理結果をコンソール出力、result.txtファイル保存
# 処理手順:
# 1. LSD実装の選択(OpenCV/pytlsd)
# 2. 動画フレーム取得
# 3. グレースケール変換とカラー勾配情報の統合
# 4. 選択したLSDアルゴリズムによる直線検出
# 5. 検出線分の描画
# 6. 結果表示・保存
# 前処理、後処理:
# 前処理(OpenCV LSD選択時): RGB各チャンネルでのSobel勾配計算、重み付きグレースケール変換、ガウシアンフィルタリング、コントラスト強化、適応的ヒストグラム均等化
# 前処理(pytlsd選択時): グレースケール変換のみ
# 後処理: 検出線分の可視化描画、線分長による絞り込み(OpenCV LSD使用時は最小長30ピクセル)
# 追加処理:
# フレーム数カウント、処理時間計測、検出線分数の統計処理、使用デバイス情報の記録
# 調整を必要とする設定値(OpenCV LSD使用時):
# - refine(2): 線分の改良レベル(0=なし、1=標準、2=高度)
# - scale(解像度適応): 画像のスケール(高解像度0.5、中解像度0.8、低解像度1.0)
# - sigma_scale(0.4): ガウシアンフィルタのスケール
# - quant(明度適応): バウンド角度の許容誤差(明るい画像2.5、暗い画像1.5)
# - ang_th(明度適応): 角度の閾値(明るい画像20.0、暗い画像15.0)
# - 線分長フィルタ: 最小長30ピクセル以上の線分のみを採用
# 算出・計算処理の検証:
# 検出された線分の座標が正しく取得され、描画処理が適切に実行されることを確認
# 将来方策: パラメータ自動調整機能、複数検出器の比較機能の実装可能
# その他の重要事項: OpenCVの実装はRafael Grompone von Gioiらの原論文のアルゴリズムを忠実に実装したもの。pytlsdは代替実装として利用可能。標準OpenCVに含まれるため追加パッケージ不要
# 前準備:
# pip install opencv-python numpy pillow
# pytlsd使用時の追加準備:
# pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# pip install pytlsd
import cv2
import tkinter as tk
from tkinter import filedialog
import urllib
import urllib.request
import numpy as np
import time
from datetime import datetime
from PIL import Image, ImageFont, ImageDraw
import os
# pytlsdは必須(エラー処理なし)
import pytlsd
# PyTorchは任意(デバイス情報用)
device = None
try:
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
except ImportError:
pass
frame_count = 0
results_log = []
lsd_method = None # 使用するLSD実装
# Proposal B: フレーム間で再利用するインスタンス(OpenCV経路用)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
lsd_detector = None # OpenCV LSDインスタンス(初回生成後に再利用)
def video_frame_processing_opencv(frame):
"""OpenCV LSDによる処理"""
global frame_count, lsd_detector
current_time = time.time()
frame_count += 1
# カラー画像情報を活用した前処理
# RGB各チャンネルを分離
b_channel, g_channel, r_channel = cv2.split(frame)
# RGB各チャンネルにSobel演算子を適用(エッジ強度の計算)
sobelR_x = cv2.Sobel(r_channel, cv2.CV_64F, 1, 0, ksize=3)
sobelR_y = cv2.Sobel(r_channel, cv2.CV_64F, 0, 1, ksize=3)
sobelR = np.sqrt(sobelR_x**2 + sobelR_y**2)
sobelG_x = cv2.Sobel(g_channel, cv2.CV_64F, 1, 0, ksize=3)
sobelG_y = cv2.Sobel(g_channel, cv2.CV_64F, 0, 1, ksize=3)
sobelG = np.sqrt(sobelG_x**2 + sobelG_y**2)
sobelB_x = cv2.Sobel(b_channel, cv2.CV_64F, 1, 0, ksize=3)
sobelB_y = cv2.Sobel(b_channel, cv2.CV_64F, 0, 1, ksize=3)
sobelB = np.sqrt(sobelB_x**2 + sobelB_y**2)
# 統合勾配マップ生成
integrated_gradient = np.sqrt(sobelR**2 + sobelG**2 + sobelB**2)
# 重み付きグレースケール変換(ITU-R BT.601規格)
gray_weighted = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 勾配情報を用いたエッジ強調
# グレースケール画像に勾配情報を重み付けして統合
gradient_normalized = cv2.normalize(integrated_gradient, None, 0, 255, cv2.NORM_MINMAX)
gradient_normalized = gradient_normalized.astype(np.uint8)
# エッジ強調されたグレースケール画像を生成(7:3の比率で統合)
gray = cv2.addWeighted(gray_weighted, 0.7, gradient_normalized, 0.3, 0)
# 前処理による精度向上
# ガウシアンフィルタリング(ノイズ除去)
gray = cv2.GaussianBlur(gray, (3, 3), 0.5)
# コントラスト強化
gray = cv2.convertScaleAbs(gray, alpha=1.2, beta=10)
# 適応的ヒストグラム均等化(局所的なコントラスト改善)を再利用インスタンスで実施
gray = clahe.apply(gray)
# LSD入力画像を保存(表示用)
lsd_input = gray.copy()
# 画像品質に応じた適応的処理(初回フレームで決定・以後固定)
if lsd_detector is None:
height, width = gray.shape
total_pixels = height * width
# 解像度別パラメータ設定
if total_pixels >= 1920 * 1080: # 高解像度(1080p以上)
scale, refine = 0.5, 2
elif total_pixels >= 1280 * 720: # 中解像度(720p)
scale, refine = 0.8, 1
else: # 低解像度(480p以下)
scale, refine = 1.0, 0
# 明度別パラメータ設定(初回フレームで固定)
mean_brightness = cv2.mean(gray)[0]
if mean_brightness > 127: # 明るい画像
ang_th, quant = 20.0, 2.5
else: # 暗い画像
ang_th, quant = 15.0, 1.5
# LSDインスタンス生成(初回のみ)
lsd_detector = cv2.createLineSegmentDetector(refine, scale, 0.4, quant, ang_th)
# 検出(以後はインスタンス再利用)
lines = lsd_detector.detect(gray)[0]
# 検出結果描画用のフレームをコピー
detection_result = frame.copy()
# 後処理フィルタリング(線分長による絞り込み)
line_count = 0
if lines is not None and len(lines) > 0:
for line in lines:
x1, y1, x2, y2 = line[0]
# 線分の長さを計算
line_length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
# 最小長閾値(30ピクセル)を適用
if line_length >= 30:
cv2.line(detection_result, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
line_count += 1
# フォント設定と描画(検出結果側のみ)
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20
if os.path.exists(FONT_PATH):
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
img_pil = Image.fromarray(cv2.cvtColor(detection_result, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
draw.text((10, 10), f"フレーム: {frame_count}, 検出線分数: {line_count} (OpenCV)", font=font, fill=(0, 255, 0))
detection_result = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
else:
# フォントが存在しない場合はOpenCVの描画を使用
cv2.putText(detection_result, f"Frame: {frame_count}, Lines: {line_count} (OpenCV)",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
# LSD入力画像をカラー化(グレースケールを3チャンネルに変換)
lsd_input_color = cv2.cvtColor(lsd_input, cv2.COLOR_GRAY2BGR)
# 左右に並べて結合
combined_frame = np.hstack([lsd_input_color, detection_result])
result = f"フレーム {frame_count}: 検出線分数 {line_count} (OpenCV LSD)"
return combined_frame, result, current_time
def video_frame_processing_pytlsd(frame):
"""pytlsd LSDによる処理"""
global frame_count
current_time = time.time()
frame_count += 1
# グレースケール変換
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# LSD入力画像を保存(表示用)
lsd_input = gray.copy()
# pytlsd による線分検出
segments = pytlsd.lsd(gray)
# 検出結果描画用のフレームをコピー
detection_result = frame.copy()
# 線分描画
line_count = 0
if segments is not None and len(segments) > 0:
for segment in segments:
x1, y1, x2, y2 = map(int, segment[:4])
cv2.line(detection_result, (x1, y1), (x2, y2), (0, 255, 0), 2)
line_count += 1
# フォント設定と描画(検出結果側のみ)
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20
if os.path.exists(FONT_PATH):
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
img_pil = Image.fromarray(cv2.cvtColor(detection_result, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
draw.text((10, 10), f"フレーム: {frame_count}, 検出線分数: {line_count} (pytlsd)", font=font, fill=(0, 255, 0))
detection_result = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
else:
# フォントが存在しない場合はOpenCVの描画を使用
cv2.putText(detection_result, f"Frame: {frame_count}, Lines: {line_count} (pytlsd)",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
# LSD入力画像をカラー化(グレースケールを3チャンネルに変換)
lsd_input_color = cv2.cvtColor(lsd_input, cv2.COLOR_GRAY2BGR)
# 左右に並べて結合
combined_frame = np.hstack([lsd_input_color, detection_result])
result = f"フレーム {frame_count}: 検出線分数 {line_count} (pytlsd)"
return combined_frame, result, current_time
print("LineSegmentDetector動画直線検出プログラム")
print("操作方法: qキーでプログラム終了")
print("表示: 左側=LSD入力画像(前処理後)、右側=検出結果")
# LSD実装の固定(pytlsdのみ使用)
print("\nLSD実装: pytlsd を使用します")
lsd_method = "pytlsd"
if device is not None:
print(f"使用デバイス: {str(device)}")
print("\n動画ソースの選択:")
print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")
choice = input("選択: ")
if choice == '0':
root = tk.Tk()
root.withdraw()
path = filedialog.askopenfilename()
if not path:
exit()
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('動画ファイル・カメラを開けませんでした')
exit()
# メイン処理
print('\n=== 動画処理開始 ===')
print('操作方法:')
print(' q キー: プログラム終了')
try:
while True:
ret, frame = cap.read()
if not ret:
break
# pytlsdのみ実行
MAIN_FUNC_DESC = "LSD直線検出 - pytlsd(左:入力画像/右:検出結果)"
processed_frame, result, current_time = video_frame_processing_pytlsd(frame)
cv2.imshow(MAIN_FUNC_DESC, processed_frame)
if choice == '1': # カメラの場合
print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], result)
else: # 動画ファイルの場合
print(frame_count, result)
results_log.append(result)
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(f'使用LSD実装: {lsd_method}\n')
if lsd_method == "pytlsd" and device is not None:
f.write(f'使用デバイス: {str(device).upper()}\n')
if device.type == 'cuda':
f.write(f'GPU: {torch.cuda.get_device_name(0)}\n')
f.write('\n')
f.write('\n'.join(results_log))
print(f'\n処理結果をresult.txtに保存しました')