feat: send beam-mark centre as image metadata (overlay, not baked)
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:
co-authored by
Claude Opus 4.8
parent
0af58574b2
commit
eaa9e1009b
+10
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user