"""
overlay
画像処理の結果をカメラ映像に重ねて描画するモジュール
チェックボックスで個別に ON/OFF できる
"""
from dataclasses import dataclass
import cv2
import numpy as np
from common.vision.fitting import theil_sen_fit
from common.vision.line_detector import LineDetectResult
# 描画色の定義 (BGR)
COLOR_LINE: tuple = (0, 255, 0)
COLOR_CENTER: tuple = (0, 255, 255)
COLOR_REGION: tuple = (255, 0, 0)
COLOR_ROW_CENTER: tuple = (0, 165, 255)
COLOR_THEIL_SEN: tuple = (255, 0, 255)
COLOR_PURSUIT: tuple = (255, 255, 0)
COLOR_INTERSECTION: tuple = (0, 0, 255)
# パシュート目標点の描画半径
PURSUIT_POINT_RADIUS: int = 2
# 二値化オーバーレイの不透明度
BINARY_OPACITY: float = 0.4
@dataclass
class OverlayFlags:
"""オーバーレイ表示項目のフラグ
Attributes:
binary: 二値化画像の半透明表示
detect_region: 検出領域の枠
poly_curve: フィッティング曲線
row_centers: 各行の線中心点
theil_sen: Theil-Sen 近似直線
center_line: 画像中心線
pursuit_points: 2点パシュートの目標点
"""
binary: bool = False
detect_region: bool = False
poly_curve: bool = False
row_centers: bool = False
theil_sen: bool = False
center_line: bool = False
pursuit_points: bool = False
def draw_overlay(
frame: np.ndarray,
result: LineDetectResult | None,
flags: OverlayFlags,
pursuit_points: (
tuple[tuple[float, float], tuple[float, float]]
| None
) = None,
is_intersection: bool = False,
) -> np.ndarray:
"""カメラ映像にオーバーレイを描画する
Args:
frame: 元の BGR カメラ画像
result: 線検出の結果(None の場合はオーバーレイなし)
flags: 表示項目のフラグ
pursuit_points: 2点パシュートの目標点
((near_x, near_y), (far_x, far_y))
is_intersection: 十字路と判定されているか
Returns:
オーバーレイ描画済みの画像
"""
display = frame.copy()
h, w = display.shape[:2]
# 十字路判定の表示
if is_intersection:
cv2.rectangle(
display, (0, 0), (w - 1, h - 1),
COLOR_INTERSECTION, 1,
)
if result is None:
return display
# 二値化画像の半透明オーバーレイ
if flags.binary and result.binary_image is not None:
display = _draw_binary_overlay(
display, result.binary_image,
)
# 検出領域の枠
if flags.detect_region:
cv2.rectangle(
display,
(0, 0),
(w - 1, h - 1),
COLOR_REGION, 1,
)
# 画像中心線
if flags.center_line:
center_x = w // 2
cv2.line(
display,
(center_x, 0),
(center_x, h),
COLOR_CENTER, 1,
)
# フィッティング曲線
if flags.poly_curve and result.poly_coeffs is not None:
_draw_poly_curve(display, result.poly_coeffs)
# 各行の線中心点
if flags.row_centers and result.row_centers is not None:
_draw_row_centers(display, result.row_centers)
# Theil-Sen 近似直線
if flags.theil_sen and result.row_centers is not None:
_draw_theil_sen_line(
display, result.row_centers,
)
# 2点パシュートの目標点
if flags.pursuit_points and pursuit_points is not None:
_draw_pursuit_points(display, pursuit_points)
return display
def _draw_binary_overlay(
frame: np.ndarray,
binary: np.ndarray,
) -> np.ndarray:
"""二値化画像を半透明で重ねる
Args:
frame: 元の BGR 画像
binary: 二値化画像(グレースケール)
Returns:
合成された画像
"""
binary_bgr = np.zeros_like(frame)
binary_bgr[:, :, 2] = binary
return cv2.addWeighted(
frame, 1.0 - BINARY_OPACITY,
binary_bgr, BINARY_OPACITY, 0,
)
def _draw_row_centers(
frame: np.ndarray,
centers: np.ndarray,
) -> None:
"""各行の線中心点を描画する
Args:
frame: 描画先の画像
centers: 各行の中心 x 座標(NaN=線なし)
"""
w = frame.shape[1]
for y, cx in enumerate(centers):
if np.isnan(cx):
continue
ix = int(round(cx))
if 0 <= ix < w:
frame[y, ix] = COLOR_ROW_CENTER
def _draw_theil_sen_line(
frame: np.ndarray,
centers: np.ndarray,
) -> None:
"""行中心点から Theil-Sen 近似直線を描画する
Args:
frame: 描画先の画像
centers: 各行の中心 x 座標(NaN=線なし)
"""
h, w = frame.shape[:2]
valid = ~np.isnan(centers)
ys = np.where(valid)[0].astype(float)
xs = centers[valid]
if len(ys) < 2:
return
slope, intercept = theil_sen_fit(ys, xs)
# 直線の両端を計算して描画
x0 = int(round(intercept))
x1 = int(round(slope * (h - 1) + intercept))
cv2.line(
frame,
(x0, 0), (x1, h - 1),
COLOR_THEIL_SEN, 1,
)
def _draw_poly_curve(
frame: np.ndarray,
coeffs: np.ndarray,
) -> None:
"""フィッティング曲線を描画する
Args:
frame: 描画先の画像
coeffs: 多項式の係数
"""
h, w = frame.shape[:2]
poly = np.poly1d(coeffs)
# 曲線上の点を生成
ys = np.arange(0, h)
xs = poly(ys)
# 画像範囲内の点のみ描画
points = []
for x, y in zip(xs, ys):
ix = int(round(x))
if 0 <= ix < w:
points.append([ix, int(y)])
if len(points) >= 2:
pts = np.array(points, dtype=np.int32)
cv2.polylines(
frame, [pts], False,
COLOR_LINE, 1,
)
def _draw_pursuit_points(
frame: np.ndarray,
points: tuple[
tuple[float, float], tuple[float, float]
],
) -> None:
"""2点パシュートの目標点を描画する
Args:
frame: 描画先の画像
points: ((near_x, near_y), (far_x, far_y))
"""
w = frame.shape[1]
for x, y in points:
ix = int(round(x))
iy = int(round(y))
if 0 <= ix < w:
cv2.circle(
frame,
(ix, iy),
PURSUIT_POINT_RADIUS,
COLOR_PURSUIT,
-1,
)