feat(flomni): live ROI size readout and camera-sourced pixel calibration
X-Ray Eye alignment GUI + script now share a single, device-configured pixel calibration and the GUI shows live ROI dimensions. GUI (x_ray_eye.py): - Add read-only "ROI size" field below the message box, showing the active ROI's x/y extent, updated live via each ROI's sigRegionChanged. Rectangles report width/height; circular ROIs read raw state["size"] so an ellipse state would be reflected (currently aspect-locked upstream). - Add "Calibration" field showing microns/pixel, or "uncalibrated (pixels)" when the camera parameter is absent. - Resolve mm/pixel from dev.cam_xeye.user_parameter["pixel_calibration"]; fall back to raw pixels (scale 1.0, unit "px") if unavailable. - Enlarge the message field from 60 to 90 px height. Alignment script (x_ray_eye_align.py): - Replace the hard-coded PIXEL_CALIBRATION constant with a pixel_calibration property reading the same cam_xeye user parameter; fall back to PIXEL_CALIBRATION_DEFAULT if unavailable. - Fold the recurring factor of 2 into the calibration: xval_x_* come straight from raw ROI pixels on submit, so the true scale is 0.05/113 mm/pixel. Default is now 0.05/113 and the three /2 factors (FZP center x, height centering, FOV) are removed. Computed physical values are unchanged. Device config (cam_xeye_pixel_calibration.yaml): - Add pixel_calibration userParameter (0.05/113 mm per raw ROI pixel) to cam_xeye. Merge the userParameter block into the existing device entry.
This commit is contained in:
@@ -28,9 +28,13 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
# pixel calibration, multiply to get mm
|
||||
# Pixel calibration in mm/pixel (multiply pixel values to get mm).
|
||||
# The live value is read from the cam_xeye device's ``pixel_calibration``
|
||||
# user parameter (see the ``pixel_calibration`` property); this constant is
|
||||
# only the fallback used when that device/parameter is unavailable.
|
||||
test_wo_movements = False
|
||||
PIXEL_CALIBRATION = 0.1 / 113 # .2 with binning
|
||||
PIXEL_CALIBRATION_DEFAULT = 0.05 / 113 # mm per raw ROI pixel (.1/113 with binning)
|
||||
PIXEL_CALIBRATION_USER_PARAM = "pixel_calibration"
|
||||
|
||||
# Sign for the automatic vertical-centering move in the height-centering
|
||||
# branch of _align_impl (search for `_height_centered`).
|
||||
@@ -62,6 +66,22 @@ class XrayEyeAlign:
|
||||
def gui(self):
|
||||
return self.flomni.xeyegui
|
||||
|
||||
@property
|
||||
def pixel_calibration(self) -> float:
|
||||
"""Pixel calibration in mm/pixel.
|
||||
|
||||
Reads the ``pixel_calibration`` user parameter from the cam_xeye device;
|
||||
falls back to ``PIXEL_CALIBRATION_DEFAULT`` if the device or parameter is
|
||||
unavailable.
|
||||
"""
|
||||
try:
|
||||
mm_per_pixel = dev.cam_xeye.user_parameter.get(self.PIXEL_CALIBRATION_USER_PARAM)
|
||||
except Exception:
|
||||
mm_per_pixel = None
|
||||
if mm_per_pixel is None:
|
||||
return self.PIXEL_CALIBRATION_DEFAULT
|
||||
return float(mm_per_pixel)
|
||||
|
||||
def _reset_init_values(self):
|
||||
self.shift_xy = [0, 0]
|
||||
self._xray_fov_xy = [0, 0]
|
||||
@@ -222,7 +242,7 @@ class XrayEyeAlign:
|
||||
if dev.omny_xray_gui.submit.get() == 1:
|
||||
|
||||
self.alignment_values[k] = (
|
||||
getattr(dev.omny_xray_gui, f"xval_x_{k}").get() / 2 * self.PIXEL_CALIBRATION
|
||||
getattr(dev.omny_xray_gui, f"xval_x_{k}").get() * self.pixel_calibration
|
||||
) # in mm
|
||||
print(f"Clicked position {k}: x {self.alignment_values[k]}")
|
||||
rtx_position = dev.rtx.readback.get() / 1000
|
||||
@@ -297,8 +317,7 @@ class XrayEyeAlign:
|
||||
delta_y_mm = (
|
||||
self.HEIGHT_CENTERING_SIGN
|
||||
* (fzp_center_y - mark_y)
|
||||
/ 2
|
||||
* self.PIXEL_CALIBRATION
|
||||
* self.pixel_calibration
|
||||
)
|
||||
print(
|
||||
f"Height centering: fzp_center_y={fzp_center_y}, mark_y={mark_y}, "
|
||||
@@ -367,8 +386,8 @@ class XrayEyeAlign:
|
||||
time.sleep(0.1)
|
||||
|
||||
self.write_output()
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovx = self._xray_fov_xy[0] * self.pixel_calibration * 1000
|
||||
fovy = self._xray_fov_xy[1] * self.pixel_calibration * 1000
|
||||
|
||||
if keep_shutter_open:
|
||||
if self.flomni.OMNYTools.yesno("Close the shutter now?", "y"):
|
||||
|
||||
@@ -260,6 +260,13 @@ class XRayEye(BECWidget, QWidget):
|
||||
ROI_LINE_COLOR = "blue"
|
||||
ROI_LINE_WIDTH = 2
|
||||
|
||||
# Image scale read from the camera's ``pixel_calibration`` user parameter
|
||||
# (mm/pixel, matching XrayEyeAlign.PIXEL_CALIBRATION in x_ray_eye_align.py).
|
||||
# If the camera device or the parameter is missing, fall back to 1 (i.e.
|
||||
# sizes are reported in raw pixels).
|
||||
PIXEL_CALIBRATION_USER_PARAM = "pixel_calibration"
|
||||
PIXEL_CALIBRATION_DEFAULT = 1.0
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self._connected_motor = None
|
||||
@@ -398,10 +405,21 @@ class XRayEye(BECWidget, QWidget):
|
||||
form.addWidget(QLabel("Sample", parent=self), 0, 0)
|
||||
form.addWidget(self.sample_name_line_edit, 0, 1)
|
||||
self.message_line_edit = QTextEdit(parent=self)
|
||||
self.message_line_edit.setFixedHeight(60)
|
||||
self.message_line_edit.setFixedHeight(90)
|
||||
self.message_line_edit.setReadOnly(True)
|
||||
form.addWidget(QLabel("Message", parent=self), 1, 0)
|
||||
form.addWidget(self.message_line_edit, 1, 1)
|
||||
# Live ROI size readout (microns), updated as the active ROI is resized.
|
||||
self.roi_size_line_edit = QLineEdit(parent=self)
|
||||
self.roi_size_line_edit.setReadOnly(True)
|
||||
form.addWidget(QLabel("ROI size", parent=self), 2, 0)
|
||||
form.addWidget(self.roi_size_line_edit, 2, 1)
|
||||
# Pixel calibration readout (microns/pixel), from the cam_xeye device.
|
||||
self.calibration_line_edit = QLineEdit(parent=self)
|
||||
self.calibration_line_edit.setReadOnly(True)
|
||||
form.addWidget(QLabel("Calibration", parent=self), 3, 0)
|
||||
form.addWidget(self.calibration_line_edit, 3, 1)
|
||||
self._update_calibration_readout()
|
||||
self.control_panel_layout.addLayout(form)
|
||||
|
||||
# Fix panel width and allow vertical expansion
|
||||
@@ -475,6 +493,61 @@ class XRayEye(BECWidget, QWidget):
|
||||
def _style_new_roi(self, roi):
|
||||
"""Force a thinner outline on newly drawn ROIs (color is set via compact_color)."""
|
||||
roi.line_width = self.ROI_LINE_WIDTH
|
||||
# Live-update the microns readout while this ROI is drawn/resized.
|
||||
roi.sigRegionChanged.connect(lambda r=roi: self._update_roi_size_readout(r))
|
||||
self._update_roi_size_readout(roi)
|
||||
|
||||
def _microns_per_pixel(self):
|
||||
"""Resolve microns/pixel and unit label from the camera user parameter.
|
||||
|
||||
Reads ``cam_xeye``'s ``pixel_calibration`` user parameter (mm/pixel) and
|
||||
converts to microns. If the camera device or the parameter is not
|
||||
available, falls back to ``PIXEL_CALIBRATION_DEFAULT`` and reports the
|
||||
unit as raw pixels.
|
||||
|
||||
Returns:
|
||||
tuple[float, str]: (scale factor applied to pixel sizes, unit label).
|
||||
"""
|
||||
try:
|
||||
cam = getattr(self.dev, CAMERA[0])
|
||||
mm_per_pixel = cam.user_parameter.get(self.PIXEL_CALIBRATION_USER_PARAM)
|
||||
except Exception:
|
||||
mm_per_pixel = None
|
||||
if mm_per_pixel is None:
|
||||
return self.PIXEL_CALIBRATION_DEFAULT, "px"
|
||||
return float(mm_per_pixel) * 1000, "um"
|
||||
|
||||
def _update_calibration_readout(self):
|
||||
"""Update the pixel-calibration readout from the camera user parameter."""
|
||||
scale, unit = self._microns_per_pixel()
|
||||
if unit == "um":
|
||||
self.calibration_line_edit.setText(f"{scale:.4f} um/pixel")
|
||||
else:
|
||||
self.calibration_line_edit.setText("uncalibrated (pixels)")
|
||||
|
||||
def _update_roi_size_readout(self, roi=None):
|
||||
"""Update the ROI size readout (microns, or pixels if uncalibrated)."""
|
||||
self._update_calibration_readout()
|
||||
if roi is None:
|
||||
roi = self.roi_manager.single_active_roi
|
||||
if roi is None:
|
||||
self.roi_size_line_edit.setText("")
|
||||
return
|
||||
if isinstance(roi, RectangularROI):
|
||||
coords = roi.get_coordinates(typed=True)
|
||||
size_x = coords["width"]
|
||||
size_y = coords["height"]
|
||||
elif isinstance(roi, CircularROI):
|
||||
# Read the raw x/y size so a non-circular (ellipse) state is reflected
|
||||
# correctly; for an aspect-locked circle both values are equal.
|
||||
size_x, size_y = roi.state["size"]
|
||||
else:
|
||||
self.roi_size_line_edit.setText("")
|
||||
return
|
||||
scale, unit = self._microns_per_pixel()
|
||||
self.roi_size_line_edit.setText(
|
||||
f"x = {abs(size_x) * scale:.1f} {unit}, y = {abs(size_y) * scale:.1f} {unit}"
|
||||
)
|
||||
|
||||
def _create_separator(self):
|
||||
sep = QFrame(parent=self)
|
||||
|
||||
@@ -494,7 +494,8 @@ cam_xeye:
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
userParameter:
|
||||
pixel_calibration: 0.00044247787610619477 # mm/pixel (= 0.05 / 113)
|
||||
# cam_ids_rgb:
|
||||
# description: Camera flOMNI Xray eye ID203
|
||||
# deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
|
||||
|
||||
Reference in New Issue
Block a user