Commit 339720b1 authored by Perceval Guillou's avatar Perceval Guillou
Browse files

store raw coords add check roi

parent 6fdbf411
......@@ -7,27 +7,151 @@
from typing import Iterable
import typeguard
from bliss.common.counter import Counter
from bliss.controllers.lima.roi import raw_roi_to_current_roi, current_roi_to_raw_roi
import numpy
from bliss.common.counter import Counter
from bliss.config.beacon_object import BeaconObject
from bliss.common.logtools import log_debug
# ========== RULES of Tango-Lima ==================
# order of image transformation:
# 0) set back binning to 1,1 before any flip or rot modif (else lima crashes if a roi/subarea is already defined))
# 1) flip [Left-Right, Up-Down] (in bin 1,1 only else lima crashes if a roi/subarea is already defined)
# 2) rotation (clockwise and negative angles not possible) (in bin 1,1 only for same reasons)
# 3) binning
# 4) roi (expressed in the current state => roi = f(flip, rot, bin))
# Lima rules and order of image transformations:
# note:
# rot 180 = flip LR + UD
# rot 270 = LR + UD + rot90
# 1) binning
# 2) flip
# 3) rotation
# 4) roi (expressed in the current state f(bin, flip, rot))
# roi is defined in the current image referential (i.e roi = f(rot, flip, bin))
# raw_roi is defined in the raw image referential (i.e with bin=1,1 flip=False,False, rot=0)
# flip = [Left-Right, Up-Down]
# ----------------- helpers for ROI coordinates (x,y,w,h) transformations (flip, rotation, binning) --------------
_DEG2RAD = numpy.pi / 180.0
def current_coords_to_raw_coords(coords_list, img_size, flip, rotation, binning):
if not isinstance(coords_list, numpy.ndarray):
pts = numpy.array(coords_list)
else:
pts = coords_list.copy()
w0, h0 = img_size
# inverse rotation
if rotation != 0:
pts = calc_pts_rotation(pts, -rotation, (w0, h0))
if rotation in [90, 270]:
w0, h0 = img_size[1], img_size[0]
# unflipped roi
if flip[0]:
pts[:, 0] = w0 - pts[:, 0]
if flip[1]:
pts[:, 1] = h0 - pts[:, 1]
# unbinned roi
xbin, ybin = binning
if xbin != 1 or ybin != 1:
pts[:, 0] = pts[:, 0] * xbin
pts[:, 1] = pts[:, 1] * ybin
return pts
def raw_coords_to_current_coords(
raw_coords_list, raw_img_size, flip, rotation, binning
):
if not isinstance(raw_coords_list, numpy.ndarray):
pts = numpy.array(raw_coords_list)
else:
pts = raw_coords_list.copy()
w0, h0 = raw_img_size
# bin roi
xbin, ybin = binning
if xbin != 1 or ybin != 1:
pts[:, 0] = pts[:, 0] / xbin
pts[:, 1] = pts[:, 1] / ybin
w0 = w0 / xbin
h0 = h0 / ybin
# flip roi
if flip[0]:
pts[:, 0] = w0 - pts[:, 0]
if flip[1]:
pts[:, 1] = h0 - pts[:, 1]
# rotate roi
if rotation != 0:
pts = calc_pts_rotation(pts, rotation, (w0, h0))
return pts
def raw_roi_to_current_roi(raw_roi, raw_img_size, flip, rotation, binning):
x, y, w, h = raw_roi
pts = [[x, y], [x + w, y + h]]
pts = raw_coords_to_current_coords(pts, raw_img_size, flip, rotation, binning)
x1, y1 = pts[0]
x2, y2 = pts[1]
x = min(x1, x2)
y = min(y1, y2)
w = abs(x2 - x1)
h = abs(y2 - y1)
return [round(x), round(y), round(w), round(h)]
def current_roi_to_raw_roi(current_roi, img_size, flip, rotation, binning):
x, y, w, h = current_roi
pts = [[x, y], [x + w, y + h]]
pts = current_coords_to_raw_coords(pts, img_size, flip, rotation, binning)
x1, y1 = pts[0]
x2, y2 = pts[1]
x = min(x1, x2)
y = min(y1, y2)
w = abs(x2 - x1)
h = abs(y2 - y1)
return [x, y, w, h]
def calc_pts_rotation(pts, angle, img_size):
if not isinstance(pts, numpy.ndarray):
pts = numpy.array(pts)
# define the camera fullframe
w0, h0 = img_size
frame = numpy.array([[0, 0], [w0, h0]])
# define the rotation matrix
theta = _DEG2RAD * angle * -1 # Lima rotation is clockwise !
R = numpy.array(
[[numpy.cos(theta), -numpy.sin(theta)], [numpy.sin(theta), numpy.cos(theta)]]
)
new_frame = numpy.dot(frame, R)
new_pts = numpy.dot(pts, R)
# find new origin
ox = numpy.amin(new_frame[:, 0])
oy = numpy.amin(new_frame[:, 1])
# apply new origin
new_pts[:, 0] = new_pts[:, 0] - ox
new_pts[:, 1] = new_pts[:, 1] - oy
return new_pts
# -------------------------------------------------------------------------------------------
def _to_list(setting, value):
......@@ -232,10 +356,7 @@ class ImageCounter(Counter):
# handle lazy init
if self._cur_roi is None:
detector_size = self._get_detector_max_size()
self._cur_roi = raw_roi_to_current_roi(
self.raw_roi, detector_size, self.flip, self.rotation, self.binning
)
self._update_roi()
return self._cur_roi
......@@ -248,6 +369,8 @@ class ImageCounter(Counter):
) # store the new _raw_roi in redis/settings
self._cur_roi = roi
self._counter_controller.roi_counters._restore_rois_from_settings()
@property
def subarea(self):
""" Returns the active area of the detector (like 'roi').
......@@ -277,6 +400,7 @@ class ImageCounter(Counter):
self._cur_roi = raw_roi_to_current_roi(
self.raw_roi, detector_size, self.flip, self.rotation, self.binning
)
self._counter_controller.roi_counters._restore_rois_from_settings()
def _calc_raw_roi(self, roi):
""" computes the raw_roi from a given roi and current bin, flip, rot """
......
......@@ -442,11 +442,10 @@ class Lima(CounterController, HasMetadataForScan):
# ------- send the params to tango-lima ---------------------------------------------
# Lima rules and order of image transformations:
# 0) set back binning to 1,1 before any flip or rot modif (else lima crashes if a roi/subarea is already defined with lima-core < 1.9.6rc3))
# 1) flip [Left-Right, Up-Down] (in bin 1,1 only else lima crashes if a roi/subarea is already defined)
# 2) rotation (clockwise and negative angles not possible) (in bin 1,1 only for same reason)
# 3) binning
# 4) roi (expressed in the current state f(flip, rot, bin))
# 1) binning
# 2) flip [Left-Right, Up-Down]
# 3) rotation (clockwise!)
# 4) roi (expressed in the current state f(bin, flip, rot))
# --- Extract special params from ctrl_params and sort them -----------
special_params = {}
......
......@@ -7,7 +7,6 @@
import enum
import itertools
import functools
import numpy
from bliss.config import settings
......@@ -16,157 +15,16 @@ from bliss.common.tango import DevFailed
from bliss.common.counter import IntegratingCounter
from bliss.controllers.counter import IntegratingCounterController
from bliss.controllers.counter import counter_namespace
from bliss.controllers.lima.image import raw_roi_to_current_roi, current_roi_to_raw_roi
from bliss.controllers.lima.image import (
current_coords_to_raw_coords,
raw_coords_to_current_coords,
)
from bliss.controllers.lima.image import _DEG2RAD
from bliss.scanning.acquisition.lima import RoiProfileAcquisitionSlave
from bliss.shell.formatters.table import IncrementalTable
# ----------------- helpers for ROI transformation (flip, rotation, binning) --------------
_DEG2RAD = numpy.pi / 180.0
_RAD2DEG = 180.0 / numpy.pi
def raw_roi_to_current_roi(raw_roi, raw_img_size, flip, rotation, binning):
""" Computes the new roi after applying the transformations {flip, rotation, binning} on the raw_roi.
args:
- raw_roi: roi coordinates expressed in the {unbinned, unflipped, unrotated} image referential (i.e camera chip size).
- raw_img_size: size of the {unbinned, unflipped, unrotated} image where the roi is defined.
- flip: flipping to apply (e.g. [1,0])
- rotation: rotation to apply
- binning: binning to apply (e.g. [1,1])
"""
assert raw_roi[2] != 0
assert raw_roi[3] != 0
new_roi = raw_roi
w0, h0 = raw_img_size
# bin roi
xbin, ybin = binning
if xbin != 1 or ybin != 1:
x, y, w, h = new_roi
new_roi = [x // xbin, y // ybin, w // xbin, h // ybin]
w0, h0 = w0 // xbin, h0 // ybin
# flip roi
if flip[0]:
x, y, w, h = new_roi
new_roi = [w0 - w - x, y, w, h]
if flip[1]:
x, y, w, h = new_roi
new_roi = [x, h0 - h - y, w, h]
# rotate roi
if rotation != 0:
new_roi = calc_roi_rotation(new_roi, rotation, (w0, h0))
x, y, w, h = new_roi
return [int(x), int(y), int(w), int(h)]
def current_roi_to_raw_roi(current_roi, img_size, flip, rotation, binning):
""" computes the raw_roi (without flip, rot, bin) from the current_roi (with flip, rot, bin)
args:
- current_roi: roi coordinates expressed in the {binned, flipped, rotated} image referential.
- img_size: the actual size of the image where the roi is defined (taking into account binning and rotation)
- flip: current image flipping (e.g. [1,0])
- rotation: current image rotation
- binning: current image binning (e.g. [1,1])
"""
assert current_roi[2] != 0
assert current_roi[3] != 0
raw_roi = [
float(current_roi[0]),
float(current_roi[1]),
float(current_roi[2]),
float(current_roi[3]),
]
w0, h0 = img_size
# inverse rotation
if rotation != 0:
raw_roi = calc_roi_rotation(raw_roi, -rotation, (w0, h0))
if rotation in [90, 270]:
w0, h0 = img_size[1], img_size[0]
# unflipped roi
if flip[0]:
x, y, w, h = raw_roi
raw_roi = [w0 - w - x, y, w, h]
if flip[1]:
x, y, w, h = raw_roi
raw_roi = [x, h0 - h - y, w, h]
# unbinned roi
xbin, ybin = binning
if xbin != 1 or ybin != 1:
x, y, w, h = raw_roi
raw_roi = x * xbin, y * ybin, w * xbin, h * ybin
return raw_roi
def calc_roi_rotation(roi, angle, img_size):
""" computes the roi rotation.
args:
- roi: roi coordinates
- angle: the angle of the rotation (degree)
- img_size: size of the image where the roi is defined
"""
assert roi[2] != 0
assert roi[3] != 0
# define the camera fullframe
w0, h0 = img_size
p0 = (0, 0)
p1 = (w0, h0)
frame = numpy.array([p0, p1], dtype="float32")
# define the subarea
x, y, w, h = roi
r0 = (x, y)
r1 = (x + w, y + h)
rect = numpy.array([r0, r1], dtype="float32")
# define the rotation matrix
theta = _DEG2RAD * angle * -1 # Lima rotation is clockwise !
R = numpy.array(
[[numpy.cos(theta), -numpy.sin(theta)], [numpy.sin(theta), numpy.cos(theta)]],
dtype="float32",
)
new_frame = numpy.dot(frame, R)
new_rect = numpy.dot(rect, R)
# find top left corner of rotated fullframe (new origin)
ox = numpy.amin(new_frame[:, 0])
oy = numpy.amin(new_frame[:, 1])
# find top left corner of the subarea and reset origin to ox,oy
x0 = numpy.amin(new_rect[:, 0]) - ox
y0 = numpy.amin(new_rect[:, 1]) - oy
# find the new subarea width
w = abs(new_rect[0, 0] - new_rect[1, 0])
h = abs(new_rect[0, 1] - new_rect[1, 1])
new_roi = [x0, y0, w, h]
return new_roi
# -------------------------------------------------------------------------------------------
class ROI_PROFILE_MODES(str, enum.Enum):
horizontal = "LINES_SUM"
vertical = "COLUMN_SUM"
......@@ -187,9 +45,9 @@ _PMODE_ALIASES = {
class _BaseRoi:
def __init__(self, name=None):
self.name = name
assert self.is_valid()
self.check_validity()
def is_valid(self):
def check_validity(self):
raise NotImplementedError
def __repr__(self):
......@@ -201,6 +59,9 @@ class _BaseRoi:
def get_coords(self):
raise NotImplementedError
def get_points(self):
raise NotImplementedError
def to_array(self):
return numpy.array(self.get_coords())
......@@ -226,8 +87,12 @@ class Roi(_BaseRoi):
def p1(self):
return (self.x + self.width, self.y + self.height)
def is_valid(self):
return self.x >= 0 and self.y >= 0 and self.width >= 0 and self.height >= 0
def check_validity(self):
if self.width <= 0:
raise ValueError(f"Roi {self.name}: width must be > 0, not {self.width}")
if self.height <= 0:
raise ValueError(f"Roi {self.name}: height must be > 0, not {self.height}")
def __repr__(self):
return "<%s,%s> <%s x %s>" % (self.x, self.y, self.width, self.height)
......@@ -243,6 +108,10 @@ class Roi(_BaseRoi):
def get_coords(self):
return [self.x, self.y, self.width, self.height]
def get_points(self):
""" return the coordinates of the top-left and bottom-right corners as a list of points """
return [self.p0, self.p1]
def to_dict(self):
return {
"kind": "rect",
......@@ -269,11 +138,21 @@ class ArcRoi(_BaseRoi):
super().__init__(name)
def is_valid(self):
ans = self.r1 >= 0 and self.r2 > 0
ans = ans and self.a1 != self.a2
ans = ans and self.r1 != self.r2
return ans
def check_validity(self):
if self.r1 < 0:
raise ValueError(
f"ArcRoi {self.name}: first radius must be >= 0, not {self.r1}"
)
if self.r2 < self.r1:
raise ValueError(
f"ArcRoi {self.name}: second radius must be >= first radius, not {self.r2}"
)
if self.a1 == self.a2:
raise ValueError(
f"ArcRoi {self.name}: first and second angles must be different"
)
def __repr__(self):
return "<%.1f, %.1f> <%.1f, %.1f> <%.1f, %.1f>" % (
......@@ -297,6 +176,42 @@ class ArcRoi(_BaseRoi):
def get_coords(self):
return [self.cx, self.cy, self.r1, self.r2, self.a1, self.a2]
def get_points(self):
""" return the coordinates of the typical points of the arc region """
cx, cy, r1, r2, a1, a2 = self.get_coords()
a3 = a1 + (a2 - a1) / 2
pts = [[cx, cy]]
ca1, ca2, ca3 = (
numpy.cos(_DEG2RAD * a1),
numpy.cos(_DEG2RAD * a2),
numpy.cos(_DEG2RAD * a3),
)
sa1, sa2, sa3 = (
numpy.sin(_DEG2RAD * a1),
numpy.sin(_DEG2RAD * a2),
numpy.sin(_DEG2RAD * a3),
)
pts.append([r1 * ca1 + cx, r1 * sa1 + cy]) # p1 => (r1, a1)
pts.append([r2 * ca1 + cx, r2 * sa1 + cy]) # p2 => (r2, a1)
pts.append([r2 * ca2 + cx, r2 * sa2 + cy]) # p3 => (r2, a2)
pts.append([r1 * ca2 + cx, r1 * sa2 + cy]) # p4 => (r1, a2)
pts.append([r2 * ca3 + cx, r2 * sa3 + cy]) # p5 => (r2, a1 + (a2 - a1) / 2)
return pts
def get_bounding_box(self):
""" return the coordinates of rectangular box that surrounds the arc roi """
pts = self.get_points()
pts = numpy.array(pts[0:]) # exclude center p0
x0 = numpy.amin(pts[:, 0])
y0 = numpy.amin(pts[:, 1])
x1 = numpy.amax(pts[:, 0])
y1 = numpy.amax(pts[:, 1])
return [[x0, y0], [x1, y1]]
def to_dict(self):
return {
"kind": "arc",
......@@ -386,6 +301,20 @@ def dict_to_roi(dico: dict) -> _BaseRoi:
return roi
def coords_to_arc(coords):
cx, cy = coords[0]
x1, y1 = coords[1]
x3, y3 = coords[2]
a1 = numpy.arctan2((y1 - cy), (x1 - cx)) / _DEG2RAD
a2 = numpy.arctan2((y3 - cy), (x3 - cx)) / _DEG2RAD
r1 = numpy.sqrt((x1 - cx) ** 2 + (y1 - cy) ** 2)
r2 = numpy.sqrt((x3 - cx) ** 2 + (y3 - cy) ** 2)
return [cx, cy, r1, r2, a1, a2]
# ============ ROI COUNTERS ===========
......@@ -568,10 +497,17 @@ class RoiCounters(IntegratingCounterController):
self.fullname, default_value="default"
)
settings_name = "%s:%s" % (self.fullname, self._current_config.get())
settings_name_raw_coords = "%s_raw_coords:%s" % (
self.fullname,
self._current_config.get(),
)
self._stored_raw_coodinates = settings.HashObjSetting(settings_name_raw_coords)
self._save_rois = settings.HashObjSetting(settings_name)
self._roi_ids = {}
self.__cached_counters = {}
self._restore_rois_from_settings()
def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params):
# avoid cyclic import
from bliss.scanning.acquisition.lima import RoiCountersAcquisitionSlave
......@@ -584,6 +520,72 @@ class RoiCounters(IntegratingCounterController):
return RoiCountersAcquisitionSlave(self, ctrl_params=ctrl_params, **acq_params)
def _store_roi(self, roi):
""" Transform roi coordinates to raw coordinates and store them as settings """
img = self._master_controller.image
if isinstance(roi, ArcRoi):
pts = roi.get_points()
pts = numpy.array([pts[0], pts[1], pts[3]])
# take into account the offset of current image roi
pts[:, 0] = pts[:, 0] + img.roi[0]
pts[:, 1] = pts[:, 1] + img.roi[1]
raw_coords = current_coords_to_raw_coords(
pts, img.fullsize, img.flip, img.rotation, img.binning
)
elif isinstance(roi, Roi):
coords = roi.get_coords()
# take into account the offset of current image roi
coords[0] = coords[0] + img.roi[0]
coords[1] = coords[1] + img.roi[1]
raw_coords = current_roi_to_raw_roi(
coords, img.fullsize, img.flip, img.rotation, img.binning
)
else:
raise ValueError(f"Unknown roi type {type(roi)}")
self._stored_raw_coodinates[roi.name] = raw_coords
self._save_rois[roi.name] = roi
def _restore_rois_from_settings(self):
img = self._master_controller.image
for name in self._stored_raw_coodinates.keys():
raw_coords = self._stored_raw_coodinates[name]
if len(raw_coords) == 4:
coords = raw_roi_to_current_roi(
raw_coords,
img._get_detector_max_size(),
img.flip,
img.rotation,
img.binning,
)
coords[0] = coords[0] - img.roi[0]
coords[1] = coords[1] - img.roi[1]
roi = Roi(*coords, name=name)
else:
coords = raw_coords_to_current_coords(
raw_coords,
img._get_detector_max_size(),
img.flip,
img.rotation,
img.binning,
)
coords = coords_to_arc(list(coords))
coords[0] = coords[0] - img.roi[0]
coords[1] = coords[1] - img.roi[1]
roi = ArcRoi(*coords, name=name)
if self._check_roi_validity(roi):
self._save_rois[name] = roi
else:
print(
f"Roi {roi.name} {roi.get_coords()} has been temporarily deactivated (outside image)"
)
del self._save_rois[name]
def _check_roi_name_is_unique(self, name):
if name in self._master_controller.roi_profiles._save_rois:
raise ValueError(
......@@ -596,6 +598,43 @@ class RoiCounters(IntegratingCounterController):
f"Names conflict: '{name}' is already used in roi_collection, please use another name"
)
def _check_roi_validity(self, roi):