| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- import sys
- import os
- import tempfile
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- import cv2
- import numpy as np
- import pytest
- from core.spatial_scanner import SpatialScanner
- from core.coord_utils import compute_sample_grid
- class FakePTZ:
- def __init__(self):
- self.positions = []
- def goto_exact_position(self, pan, tilt, zoom):
- self.positions.append((pan, tilt, zoom))
- def test_spatial_scanner_grid(tmp_path):
- ptz = FakePTZ()
- counter = {"n": 0}
- def frame_source():
- counter["n"] += 1
- return np.zeros((120, 160, 3), dtype=np.uint8)
- scanner = SpatialScanner("g1", ptz, frame_source, str(tmp_path), stabilize_time=0.0)
- result = scanner.run(pan_range=(0, 90), tilt_layers=(-10, 0), pan_step=30, zoom=1)
- assert len(result["samples"]) == 6
- assert result["panorama_path"] is not None
- assert len(ptz.positions) == 6
- def test_invalid_pan_step_raises_value_error(tmp_path):
- ptz = FakePTZ()
- scanner = SpatialScanner("g1", ptz, lambda: None, str(tmp_path), stabilize_time=0.0)
- with pytest.raises(ValueError, match="pan_step must be positive"):
- scanner.run(pan_range=(0, 90), tilt_layers=(-10, 0), pan_step=0)
- def test_compute_sample_grid_validation():
- with pytest.raises(ValueError, match="pan_step must be positive"):
- compute_sample_grid(pan_step=0)
- with pytest.raises(ValueError, match="tilt_layers must not be empty"):
- compute_sample_grid(tilt_layers=())
- with pytest.raises(ValueError, match="pan_range start must be less than end"):
- compute_sample_grid(pan_range=(180.0, 180.0))
- def test_cancellation_stops_early(tmp_path):
- ptz = FakePTZ()
- counter = {"n": 0}
- def frame_source():
- counter["n"] += 1
- if counter["n"] == 2:
- scanner.cancel()
- return np.zeros((120, 160, 3), dtype=np.uint8)
- scanner = SpatialScanner("g1", ptz, frame_source, str(tmp_path), stabilize_time=0.0)
- result = scanner.run(pan_range=(0, 60), tilt_layers=(-10, 0), pan_step=30, zoom=1)
- assert len(result["samples"]) < 4
- assert scanner.progress["state"] == "cancelled"
- def test_empty_sample_list_returns_no_panorama(tmp_path):
- ptz = FakePTZ()
- scanner = SpatialScanner("g1", ptz, lambda: None, str(tmp_path), stabilize_time=0.0)
- scanner._wait_frame = lambda timeout: None
- result = scanner.run(pan_range=(0, 60), tilt_layers=(-10, 0), pan_step=30, zoom=1)
- assert result["samples"] == []
- assert result["panorama_path"] is None
- assert scanner.progress["current"] == 0
- def test_progress_callback_invoked(tmp_path):
- ptz = FakePTZ()
- progress_snapshots = []
- def frame_source():
- return np.zeros((120, 160, 3), dtype=np.uint8)
- def progress_callback(progress):
- progress_snapshots.append(dict(progress))
- scanner = SpatialScanner("g1", ptz, frame_source, str(tmp_path), stabilize_time=0.0)
- result = scanner.run(
- pan_range=(0, 60),
- tilt_layers=(-10, 0),
- pan_step=30,
- zoom=1,
- progress_callback=progress_callback,
- )
- expected_samples = len(result["samples"])
- assert expected_samples > 0
- # 扫描开始前会报告一次初始进度,之后每成功采集一个点报告一次
- assert len(progress_snapshots) == expected_samples + 1
- assert progress_snapshots[0]["current"] == 0
- assert progress_snapshots[0]["state"] == "scanning"
- assert progress_snapshots[-1]["current"] == expected_samples
- assert progress_snapshots[-1]["state"] == "scanning"
- def test_prerun_cancellation_returns_empty_result(tmp_path):
- ptz = FakePTZ()
- scanner = SpatialScanner("g1", ptz, lambda: None, str(tmp_path), stabilize_time=0.0)
- scanner.cancel()
- result = scanner.run(pan_range=(0, 60), tilt_layers=(-10, 0), pan_step=30, zoom=1)
- assert result["samples"] == []
- assert result["panorama_path"] is None
- assert scanner.progress["state"] == "cancelled"
- def test_panorama_does_not_blend_overlapping_samples(tmp_path):
- """重叠区域应直接覆盖,而不是加权融合导致虚化。"""
- ptz = FakePTZ()
- calls = {"n": 0}
- def frame_source():
- calls["n"] += 1
- # 第一张红色,第二张蓝色,两者水平视场约 55°,在 0°/30° 处明显重叠
- color = (0, 0, 255) if calls["n"] == 1 else (255, 0, 0)
- return np.full((100, 100, 3), color, dtype=np.uint8)
- scanner = SpatialScanner("g1", ptz, frame_source, str(tmp_path), stabilize_time=0.0)
- result = scanner.run(pan_range=(0, 60), tilt_layers=(0,), pan_step=30, zoom=1)
- assert result["panorama_path"] is not None
- panorama = cv2.imread(result["panorama_path"])
- assert panorama is not None
- # 在第二张图(pan=30°)的中心区域采样;直接覆盖应接近蓝色
- height, width = panorama.shape[:2]
- u = int((30 / 60) * width)
- v = int(((90 - 0) / 180) * height)
- roi = panorama[
- max(0, v - 50):min(height, v + 50),
- max(0, u - 50):min(width, u + 50),
- ]
- mask = roi.sum(axis=2) > 0
- assert mask.sum() > 0
- mean_color = roi[mask].mean(axis=0)
- blue = np.array([255, 0, 0], dtype=np.float32)
- blend = np.array([127, 0, 127], dtype=np.float32)
- dist_to_blue = np.linalg.norm(mean_color - blue)
- dist_to_blend = np.linalg.norm(mean_color - blend)
- # 均值应更接近蓝色,而不是红蓝融合色
- assert dist_to_blue < 60
- assert dist_to_blend > 90
|