DAQ: updated face_detection added operations/face_detection fodler including service and models, new tests of face_detection
This commit is contained in:
1 parent
2b7c634227
commit
a00f0b3dff
6 files changed
+783
-206
No files matched your search
@@ -0,0 +1,216 @@
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from aare.common.coordinate import Coordinate, SmargonCoordinate
|
||||
from aare.common.models import BoundingBoxModel, MLBoxModel, MLBoxType, ZoomModeEnum
|
||||
from aare.daq.operations.face_detection.models import (
|
||||
FaceDetectionContext,
|
||||
FaceDetectionResult,
|
||||
)
|
||||
from aare.daq.operations.face_detection.service import FaceDetectionService
|
||||
|
||||
|
||||
class DummyLogger:
|
||||
def info(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def debug(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def log(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class DummyGeometry:
|
||||
def __init__(self):
|
||||
self.beam_location_pxl = Coordinate(x=100.0, y=100.0, z=0.0)
|
||||
|
||||
def picture_to_smargon(self, coord: Coordinate) -> Coordinate:
|
||||
return Coordinate(x=coord.x / 100.0, y=coord.y / 100.0, z=0.0)
|
||||
|
||||
|
||||
def _box(cls: MLBoxType, x1: float, y1: float, x2: float, y2: float) -> MLBoxModel:
|
||||
return MLBoxModel(
|
||||
cls=cls,
|
||||
box=BoundingBoxModel(
|
||||
top_x=x1,
|
||||
top_y=y1,
|
||||
bottom_x=x2,
|
||||
bottom_y=y2,
|
||||
),
|
||||
conf=0.95,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def context():
|
||||
progress_events = []
|
||||
|
||||
devs = types.SimpleNamespace(
|
||||
lamp_light=0.0,
|
||||
zoom=200.0,
|
||||
aerotech_omega=0.0,
|
||||
smargon_pos=None,
|
||||
set_zoom=lambda zoom_value, wait=True: None,
|
||||
smargon_wait=lambda timeout=60: None,
|
||||
)
|
||||
|
||||
cfg = types.SimpleNamespace(zoom_mode=ZoomModeEnum.User)
|
||||
|
||||
ctx = FaceDetectionContext(
|
||||
cfg=cfg,
|
||||
devs=devs,
|
||||
mlbox=types.SimpleNamespace(
|
||||
predict=lambda **kwargs: types.SimpleNamespace(
|
||||
box=None,
|
||||
target_point=None,
|
||||
focus=None,
|
||||
)
|
||||
),
|
||||
sample_geometry_provider=lambda: DummyGeometry(),
|
||||
emit_progress=lambda payload: progress_events.append(payload),
|
||||
)
|
||||
ctx._progress_events = progress_events
|
||||
return ctx
|
||||
|
||||
|
||||
def test_service_returns_empty_payload_when_no_boxes(monkeypatch, context):
|
||||
service = FaceDetectionService(context=context, logger=DummyLogger())
|
||||
|
||||
monkeypatch.setattr(
|
||||
context.mlbox,
|
||||
"predict",
|
||||
lambda **kwargs: types.SimpleNamespace(
|
||||
box=None,
|
||||
target_point=None,
|
||||
focus=None,
|
||||
),
|
||||
)
|
||||
|
||||
result = service.run(steps=2, step_size=30)
|
||||
|
||||
assert isinstance(result, FaceDetectionResult)
|
||||
assert result.success is True
|
||||
assert result.payload["running"] is False
|
||||
assert result.payload["samples"] == []
|
||||
assert result.payload["height_fit"] == {}
|
||||
assert result.payload["area_fit"] == {}
|
||||
assert context.cfg.zoom_mode == ZoomModeEnum.User
|
||||
assert len(context._progress_events) >= 1
|
||||
assert context._progress_events[-1]["running"] is False
|
||||
|
||||
|
||||
def test_service_prefers_face_boxes_when_ratio_is_high(monkeypatch, context):
|
||||
service = FaceDetectionService(context=context, logger=DummyLogger())
|
||||
|
||||
predictions = iter(
|
||||
[
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), target_point=None, focus=None),
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 12, 20, 32, 40), target_point=None, focus=None),
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), target_point=None, focus=None),
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 16, 20, 36, 40), target_point=None, focus=None),
|
||||
]
|
||||
)
|
||||
|
||||
monkeypatch.setattr(context.mlbox, "predict", lambda **kwargs: next(predictions))
|
||||
|
||||
monkeypatch.setattr(
|
||||
"aare.daq.operations.face_detection.service.fd.get_flat_face",
|
||||
lambda boxes, start_angle, end_angle, use_area: (
|
||||
45 if use_area else 50,
|
||||
{"A": 1.0, "B": 2.0, "phi_rad": 0.1, "C": 3.0},
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aare.daq.operations.face_detection.service.fd.choose_best_fit",
|
||||
lambda fit_results: (47, {"A": 1.0}, "Area"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aare.daq.operations.face_detection.service.fd.get_samples_out",
|
||||
lambda boxes: [{"angle": angle, "box": box} for angle, box in sorted(boxes.items())],
|
||||
)
|
||||
|
||||
result = service.run(steps=3, step_size=30, face_min_ratio=0.5)
|
||||
|
||||
assert result.success is True
|
||||
assert result.payload["running"] is False
|
||||
assert result.payload["height_fit"]["best_angle_deg"] == 50
|
||||
assert result.payload["area_fit"]["best_angle_deg"] == 45
|
||||
assert context.devs.aerotech_omega == 47
|
||||
assert [sample["angle"] for sample in result.payload["samples"]] == [0, 30, 90]
|
||||
assert context._progress_events[-1]["running"] is False
|
||||
|
||||
|
||||
def test_service_falls_back_to_loop_all_when_face_ratio_is_low(monkeypatch, context):
|
||||
service = FaceDetectionService(context=context, logger=DummyLogger())
|
||||
|
||||
predictions = iter(
|
||||
[
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), target_point=None, focus=None),
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 11, 20, 31, 40), target_point=None, focus=None),
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 12, 20, 32, 40), target_point=None, focus=None),
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 13, 20, 33, 40), target_point=None, focus=None),
|
||||
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), target_point=None, focus=None),
|
||||
]
|
||||
)
|
||||
|
||||
monkeypatch.setattr(context.mlbox, "predict", lambda **kwargs: next(predictions))
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_get_flat_face(boxes, start_angle, end_angle, use_area):
|
||||
captured["boxes_used"] = dict(boxes)
|
||||
return 60, {"A": 1.0, "B": 2.0, "phi_rad": 0.2, "C": 4.0}
|
||||
|
||||
monkeypatch.setattr("aare.daq.operations.face_detection.service.fd.get_flat_face", fake_get_flat_face)
|
||||
monkeypatch.setattr(
|
||||
"aare.daq.operations.face_detection.service.fd.choose_best_fit",
|
||||
lambda fit_results: (60, {"A": 1.0}, "Height"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aare.daq.operations.face_detection.service.fd.get_samples_out",
|
||||
lambda boxes: [{"angle": angle} for angle in sorted(boxes.keys())],
|
||||
)
|
||||
|
||||
result = service.run(steps=4, step_size=30, face_min_ratio=0.5)
|
||||
|
||||
assert result.success is True
|
||||
assert len(captured["boxes_used"]) == 4
|
||||
assert sorted(captured["boxes_used"].keys()) == [30, 60, 90, 120]
|
||||
assert context.devs.aerotech_omega == 60
|
||||
assert context._progress_events[-1]["running"] is False
|
||||
|
||||
|
||||
def test_service_applies_centre_correction_when_target_is_far_from_beam(monkeypatch, context):
|
||||
service = FaceDetectionService(context=context, logger=DummyLogger())
|
||||
|
||||
model = _box(MLBoxType.LOOP_FACE, 40, 160, 80, 200)
|
||||
|
||||
service._centre_correction(model, tolerance=0.2)
|
||||
|
||||
assert isinstance(context.devs.smargon_pos, SmargonCoordinate)
|
||||
assert context.devs.smargon_pos.sh_mm.x == pytest.approx(1.0)
|
||||
assert context.devs.smargon_pos.sh_mm.y == pytest.approx(1.8)
|
||||
|
||||
|
||||
def test_service_returns_failed_result_when_prediction_raises(monkeypatch, context):
|
||||
service = FaceDetectionService(context=context, logger=DummyLogger())
|
||||
|
||||
monkeypatch.setattr(
|
||||
context.mlbox,
|
||||
"predict",
|
||||
lambda **kwargs: (_ for _ in ()).throw(RuntimeError("prediction failed")),
|
||||
)
|
||||
|
||||
result = service.run(steps=1, step_size=30)
|
||||
|
||||
assert result.success is False
|
||||
assert isinstance(result.error, RuntimeError)
|
||||
assert result.comment == "Face detection sequence failed"
|
||||
assert result.payload["running"] is False
|
||||
assert context.cfg.zoom_mode == ZoomModeEnum.User
|
||||
assert context._progress_events[-1]["running"] is False
|
||||
@@ -0,0 +1,105 @@
|
||||
import types
|
||||
|
||||
from aare.common.models import DAQOperation
|
||||
from aare.daq.operations.face_detection.models import FaceDetectionResult
|
||||
|
||||
|
||||
def test_execute_face_detection_reports_failure(monkeypatch):
|
||||
from aare.daq.daq import AareDAQ
|
||||
|
||||
daq = object.__new__(AareDAQ)
|
||||
|
||||
calls = {"set_state": [], "handle_error": []}
|
||||
|
||||
monkeypatch.setattr(
|
||||
daq,
|
||||
"_AareDAQ__set_state",
|
||||
lambda state: calls["set_state"].append(state),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
daq,
|
||||
"_create_face_detection_service",
|
||||
lambda: types.SimpleNamespace(
|
||||
run=lambda **kwargs: FaceDetectionResult(
|
||||
success=False,
|
||||
payload={"running": False, "samples": [], "height_fit": {}, "area_fit": {}},
|
||||
error=RuntimeError("fd failed"),
|
||||
comment="face detection sequence failed",
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
daq,
|
||||
"_handle_operation_error",
|
||||
lambda **kwargs: calls["handle_error"].append(kwargs),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
type(daq),
|
||||
"sample",
|
||||
property(lambda self: types.SimpleNamespace(sample_name="sample-1", db_id=1)),
|
||||
)
|
||||
|
||||
result = daq._execute_face_detection(steps=7, step_size=30, report_error=True)
|
||||
|
||||
assert result.success is False
|
||||
assert len(calls["set_state"]) == 1
|
||||
assert len(calls["handle_error"]) == 1
|
||||
assert calls["handle_error"][0]["operation"] == DAQOperation.FACE_CENTERING
|
||||
|
||||
|
||||
def test_execute_face_detection_can_skip_error_reporting(monkeypatch):
|
||||
from aare.daq.daq import AareDAQ
|
||||
|
||||
daq = object.__new__(AareDAQ)
|
||||
|
||||
handle_error_calls = []
|
||||
|
||||
monkeypatch.setattr(daq, "_AareDAQ__set_state", lambda state: None)
|
||||
monkeypatch.setattr(
|
||||
daq,
|
||||
"_create_face_detection_service",
|
||||
lambda: types.SimpleNamespace(
|
||||
run=lambda **kwargs: FaceDetectionResult(
|
||||
success=False,
|
||||
payload={"running": False, "samples": [], "height_fit": {}, "area_fit": {}},
|
||||
error=RuntimeError("fd failed"),
|
||||
comment="face detection sequence failed",
|
||||
)
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
daq,
|
||||
"_handle_operation_error",
|
||||
lambda **kwargs: handle_error_calls.append(kwargs),
|
||||
)
|
||||
|
||||
result = daq._execute_face_detection(report_error=False)
|
||||
|
||||
assert result.success is False
|
||||
assert handle_error_calls == []
|
||||
|
||||
|
||||
def test_public_face_detection_uses_execute_face_detection(monkeypatch):
|
||||
from aare.daq.daq import AareDAQ
|
||||
|
||||
daq = object.__new__(AareDAQ)
|
||||
cfg = types.SimpleNamespace(
|
||||
try_set_busy=lambda timeout=360: None,
|
||||
state_busy=False,
|
||||
)
|
||||
setattr(daq, "_AareDAQ__cfg", cfg)
|
||||
|
||||
monkeypatch.setattr(
|
||||
daq,
|
||||
"_execute_face_detection",
|
||||
lambda **kwargs: FaceDetectionResult(
|
||||
success=True,
|
||||
payload={"running": False, "samples": [{"angle": 45}], "height_fit": {}, "area_fit": {}},
|
||||
),
|
||||
)
|
||||
|
||||
result = daq.face_detection(steps=7, step_size=30)
|
||||
|
||||
assert result["running"] is False
|
||||
assert result["samples"] == [{"angle": 45}]
|
||||
assert cfg.state_busy is False
|
||||
Reference in new issue
Block a user