feat: send beam-mark centre as image metadata (overlay, not baked)
Build and Publish / test (push) Successful in 2m13s
Build and Publish / build (push) Successful in 18s
Build and Publish / Build and Deploy Docs (push) Successful in 38s

Sample screenshots now carry the beam mark as normalized metadata for the
AareDB frontend overlay — the JPEG stays unmarked so raw images remain
clean for YOLO training.

- upload_image: optional `beam_center` sent as a JSON form field.
- ScreenshotService.save_to_db: normalizes the beam mark by the captured
  frame dims -> {x,y,ar,[w,h]} via _beam_center_metadata (+ unit tests).
- save_screenshot_db: reads get_beam_mark() (centre) best-effort. Beam
  SIZE in px is a TODO(calibration) hook -> crosshair until filled, then
  a rectangle matching the gridscan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GotthardG
2026-07-01 23:23:41 +02:00
co-authored by Claude Opus 4.8
parent 0af58574b2
commit eaa9e1009b
4 changed files with 89 additions and 7 deletions
+10 -3
View File
@@ -131,7 +131,7 @@ class AareWrapper:
logger.error(f"Error sending sample event {event_type!s} to db: {e}")
@log_timing(logger, "AareDB call")
def upload_image(self, sample_id: int, filename: str, bgr_image: np.ndarray, message: Optional[str] = None):
def upload_image(self, sample_id: int, filename: str, bgr_image: np.ndarray, message: Optional[str] = None, beam_center: Optional[dict] = None):
_, buffer = cv2.imencode('.jpg', bgr_image)
jpeg_bytes = io.BytesIO(buffer)
url = f"{self.__host}/protected_router/sample_runner/{sample_id}/upload-images"
@@ -140,14 +140,21 @@ class AareWrapper:
"X-Shared-Password": os.getenv("AAREDB_SHARED_PASSWORD")
}
data = {}
if message is not None:
data["comment"] = message
if beam_center is not None:
# Overlay metadata only — the JPEG stays unmarked (kept clean for YOLO).
data["beam_center"] = json.dumps(beam_center)
request_kwargs = {
"files": {'uploaded_file': (filename + ".jpg", jpeg_bytes, "image/jpeg")},
"verify": self.__ssl_ca_cert,
"cert": (self.__cert_file, self.__key_file),
"headers": headers,
}
if message is not None:
request_kwargs["data"] = {"comment": message}
if data:
request_kwargs["data"] = data
response = requests.post(url, **request_kwargs)
logger.debug(f"Response status code: {response.status_code}")
+22 -1
View File
@@ -2506,7 +2506,28 @@ class AareDAQ:
filename: Name to give to the uploaded image.
settle_time_s: float time to wait before taking the screenshot.
"""
self._screenshot_service.save_to_db(sample_id, filename, settle_time_s)
# Beam-mark centre in sample-camera pixels at the current zoom, for the
# DB overlay (drawn in the frontend, never baked into the JPEG). Best
# effort — a calibration hiccup must never block the screenshot upload.
beam_mark_pxl = None
beam_size_pxl = None
try:
beam_mark_pxl = self.get_beam_mark()
# TODO(calibration): beam SIZE in sample-camera pixels so the overlay
# is a rectangle instead of a crosshair, e.g.
# px_per_mm = ... # sample-camera scale at self.__devs.zoom
# bs = self.__cfg.beam_size_mm
# beam_size_pxl = (bs.x * px_per_mm, bs.y * px_per_mm)
except Exception:
logger.warning("Could not read beam mark for screenshot overlay", exc_info=True)
self._screenshot_service.save_to_db(
sample_id,
filename,
settle_time_s,
beam_mark_pxl=beam_mark_pxl,
beam_size_pxl=beam_size_pxl,
)
def send_screenshot_db(self, filename: str | None = None, message: str | None = None) -> None:
sample = self.sample
+33 -2
View File
@@ -68,13 +68,44 @@ class ScreenshotService:
self.logger.debug(f"saving screenshot {filename} from inference image")
cv2.imwrite(f"{self.output_dir}/{filename}.jpg", bgr_image)
def save_to_db(self, sample_id: int, filename: str, settle_time_s: float = 0.2) -> None:
@staticmethod
def _beam_center_metadata(bgr_image, beam_mark_pxl, beam_size_pxl):
"""Normalized beam-mark rectangle {x, y, w, h, ar} (0-1) for the DB
overlay, or None. `beam_mark_pxl` = (x, y) centre in image pixels;
`beam_size_pxl` = (w, h) size in image pixels (optional -> crosshair)."""
if beam_mark_pxl is None or bgr_image is None:
return None
try:
h, w = bgr_image.shape[:2]
if not w or not h:
return None
data = {
"x": float(beam_mark_pxl[0]) / w,
"y": float(beam_mark_pxl[1]) / h,
"ar": float(w) / float(h),
}
if beam_size_pxl is not None:
data["w"] = float(beam_size_pxl[0]) / w
data["h"] = float(beam_size_pxl[1]) / h
return data
except (TypeError, ValueError, IndexError, ZeroDivisionError):
return None
def save_to_db(
self,
sample_id: int,
filename: str,
settle_time_s: float = 0.2,
beam_mark_pxl=None,
beam_size_pxl=None,
) -> None:
time.sleep(settle_time_s)
sample = self._current_sample()
bgr_image = self._get_inference_image()
beam_center = self._beam_center_metadata(bgr_image, beam_mark_pxl, beam_size_pxl)
def _upload() -> None:
self.aare.upload_image(sample_id, filename, bgr_image)
self.aare.upload_image(sample_id, filename, bgr_image, beam_center=beam_center)
self.run_noncritical(
_upload,
@@ -102,4 +102,27 @@ def test_send_to_db_requires_mounted_sample(mock_logger):
)
with pytest.raises(ValueError, match="valid sample_id"):
service.send_to_db(default_message="x")
service.send_to_db(default_message="x")
def test_beam_center_metadata_centre_only():
img = np.zeros((100, 200, 3), dtype=np.uint8) # h=100, w=200 -> ar 2.0
bc = ScreenshotService._beam_center_metadata(img, (100, 50), None)
assert bc == pytest.approx({"x": 0.5, "y": 0.5, "ar": 2.0})
def test_beam_center_metadata_with_size():
img = np.zeros((100, 200, 3), dtype=np.uint8)
bc = ScreenshotService._beam_center_metadata(img, (100, 50), (20, 10))
assert bc == pytest.approx({"x": 0.5, "y": 0.5, "ar": 2.0, "w": 0.1, "h": 0.1})
def test_beam_center_metadata_none_without_mark_or_image():
img = np.zeros((10, 10, 3), dtype=np.uint8)
assert ScreenshotService._beam_center_metadata(img, None, None) is None
assert ScreenshotService._beam_center_metadata(None, (1, 2), None) is None
def test_beam_center_metadata_tolerates_bad_input():
img = np.zeros((10, 10, 3), dtype=np.uint8)
assert ScreenshotService._beam_center_metadata(img, ("a", "b"), None) is None