Newer
Older
RobotCar / tests / test_morphology.py
"""morphology モジュールのテスト"""

import numpy as np
import pytest

from common import config
from pc.vision.morphology import (
    apply_dist_mask,
    apply_iso_closing,
    apply_staged_closing,
    apply_width_filter,
)


class TestApplyIsoClosing:
    """apply_iso_closing のテスト"""

    def test_fills_small_hole(
        self, binary_with_hole: np.ndarray,
    ) -> None:
        """小さい穴をクロージングで埋められる"""
        result = apply_iso_closing(binary_with_hole, 5)
        h = config.FRAME_HEIGHT
        w = config.FRAME_WIDTH
        # 穴があった中央付近が白になっている
        assert result[h // 2, w // 2] == 255

    def test_small_size_noop(self) -> None:
        """size < 3 の場合は何もしない"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        binary[5, 5] = 255
        result = apply_iso_closing(binary, 1)
        assert np.array_equal(result, binary)

    def test_preserves_shape(
        self, binary_with_hole: np.ndarray,
    ) -> None:
        """出力画像のサイズが入力と同じ"""
        result = apply_iso_closing(binary_with_hole, 7)
        assert result.shape == binary_with_hole.shape


class TestApplyStagedClosing:
    """apply_staged_closing のテスト"""

    def test_removes_small_regions(self) -> None:
        """min_area で小さい領域を除去できる"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        # 大きな領域
        binary[2:h - 2, w // 2 - 3 : w // 2 + 4] = 255
        # 小さな孤立点
        binary[0, 0] = 255
        result = apply_staged_closing(
            binary, small_size=3, min_area=5,
            large_size=0,
        )
        assert result[0, 0] == 0  # 孤立点が除去されている
        assert result[h // 2, w // 2] == 255  # 大領域は残る

    def test_no_min_area(self) -> None:
        """min_area=0 なら孤立除去をスキップする"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        binary[0, 0] = 255
        result = apply_staged_closing(
            binary, small_size=1, min_area=0,
            large_size=0,
        )
        assert result[0, 0] == 255


class TestApplyWidthFilter:
    """apply_width_filter のテスト"""

    def test_keeps_narrow_line(self) -> None:
        """期待幅以内の線は残す"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        cx = w // 2
        binary[:, cx - 1 : cx + 2] = 255  # 幅 3
        result = apply_width_filter(
            binary, width_near=10, width_far=5,
            tolerance=2.0,
        )
        # 幅 3 は期待幅内なので残る
        assert np.any(result[h // 2] > 0)

    def test_removes_wide_row(self) -> None:
        """期待幅を超える行を除去する"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        # 下端に幅いっぱいの白(幅=w)
        binary[h - 1, :] = 255
        result = apply_width_filter(
            binary, width_near=3, width_far=2,
            tolerance=1.5,
        )
        # 幅 w >> 3*1.5 なので除去される
        assert np.all(result[h - 1] == 0)

    def test_empty_rows_unchanged(self) -> None:
        """白ピクセルがない行はそのまま"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        result = apply_width_filter(
            binary, width_near=5, width_far=3,
            tolerance=2.0,
        )
        assert np.array_equal(result, binary)


class TestApplyDistMask:
    """apply_dist_mask のテスト"""

    def test_keeps_center_of_thick_line(self) -> None:
        """太い線の中心部を残す"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        cx = w // 2
        binary[:, cx - 5 : cx + 6] = 255  # 幅 11
        result = apply_dist_mask(binary, thresh=2.0)
        # 中心は残る
        assert result[h // 2, cx] > 0

    def test_thresh_zero_noop(self) -> None:
        """thresh <= 0 の場合は何もしない"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        binary[5, 5] = 255
        result = apply_dist_mask(binary, thresh=0.0)
        assert np.array_equal(result, binary)

    def test_removes_thin_line(self) -> None:
        """細い線(幅1px)は距離変換で除去される"""
        h, w = config.FRAME_HEIGHT, config.FRAME_WIDTH
        binary = np.zeros((h, w), dtype=np.uint8)
        binary[:, w // 2] = 255  # 幅 1px
        result = apply_dist_mask(binary, thresh=1.0)
        # 幅 1px の距離変換最大値は 1.0 未満
        assert np.all(result == 0)