"""morphology モジュールのテスト"""
import numpy as np
import pytest
from common import config
from common.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)