From 8fbd1c56af9be1a8a2acf49ac34f72d6b8243773 Mon Sep 17 00:00:00 2001 From: Thierry Zamofing Date: Thu, 25 Aug 2022 17:17:54 +0200 Subject: [PATCH] add ModuleFixTarget tab --- ModuleFixTarget.py | 784 +++++++++++++++++++++++++++++++++++++++++++++ camera.py | 4 +- pyqtUsrObj.py | 44 +++ swissmx.py | 101 ++++-- 4 files changed, 909 insertions(+), 24 deletions(-) create mode 100644 ModuleFixTarget.py diff --git a/ModuleFixTarget.py b/ModuleFixTarget.py new file mode 100644 index 0000000..487de8e --- /dev/null +++ b/ModuleFixTarget.py @@ -0,0 +1,784 @@ +#!/usr/bin/env python +# *-----------------------------------------------------------------------* +# | | +# | Copyright (c) 2022 by Paul Scherrer Institute (http://www.psi.ch) | +# | | +# | Author Thierry Zamofing (thierry.zamofing@psi.ch) | +# *-----------------------------------------------------------------------* +''' +Module to handle FixTarget acquisition + +This contains a Widget to handle FixTargetFrames and fiducials, calculate final target positions etc. +''' + +#https://stackoverflow.com/questions/27909658/json-encoder-and-decoder-for-complex-numpy-arrays + +# import yaml +# class Dice(tuple): +# def __new__(cls, a, b): +# return tuple.__new__(cls, [a, b]) +# def __repr__(self): +# return "Dice(%s,%s)" % self +# d=Dice(3,6) +# print(d) +# print(yaml.dump(d)) +# def dice_representer(dumper, data): +# return dumper.represent_scalar(u'!dice', u'%sd%s' % data) +# yaml.add_representer(Dice, dice_representer) +# def dice_constructor(loader, node): +# value = loader.construct_scalar(node) +# a, b = map(int, value.split('d')) +# return Dice(a, b) +# yaml.add_constructor(u'!dice', dice_constructor) +# print(yaml.load("initial hit points: !dice 8d4")) + +import logging +_log=logging.getLogger(__name__) + +import os, pickle, json, yaml,base64 +import numpy as np +import pyqtgraph as pg +from PyQt5.QtCore import Qt, QFileInfo, pyqtSignal, pyqtSlot +from PyQt5.QtWidgets import ( + QTableWidgetItem, + QFileDialog, + QGroupBox, + QWidget, + QGridLayout, + QTableWidget, + QAbstractItemView, + QItemDelegate, + QVBoxLayout, + QLabel, + QPushButton, + QApplication, + QMainWindow, + QHeaderView, + QLineEdit, + QSpinBox, + QMessageBox, + QAction, + QComboBox, + QCheckBox) + + +class MyJsonEncoder(json.JSONEncoder): + """ Special json encoder for numpy types """ + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + data_b64=base64.b64encode(obj.data) + return dict(__ndarray__=data_b64,dtype=str(obj.dtype),shape=obj.shape) + #return obj.tolist() + elif isinstance(obj, set): + return list(obj) + if type(obj) not in (dict,list,str,int): + _log.error('dont know how to json') + return repr(obj) + return json.JSONEncoder.default(self, obj) + +def MyJsonDecoder(dct): + if isinstance(dct, dict) and '__ndarray__' in dct: + data = base64.b64decode(dct['__ndarray__']) + return np.frombuffer(data[:-1], dct['dtype']).reshape(dct['shape']) + return dct + +def yaml_repr_numpy(dumper, data): + return dumper.represent_scalar(u'!np_array', u'%s' % repr(data)) +yaml.add_representer(np.ndarray, yaml_repr_numpy) + +def yaml_cnstr_numpy(loader, node): + value = loader.construct_scalar(node) + a=eval(value[6:-1]) + return np.array(a) +yaml.add_constructor(u'!np_array', yaml_cnstr_numpy) + + + + +class MarkerDelegate(QItemDelegate): + def createEditor(self, parent, option, index): + comboBox=QLineEdit(parent) + comboBox.editingFinished.connect(self.emitCommitData) + return comboBox + + def setEditorData(self, editor, index): + # pos = comboBox.findText(index.model().data(index), Qt.MatchExactly) + # comboBox.setCurrentIndex(pos) + editor.setText("{}".format(index.model().data(index))) + + def setModelData(self, editor, model, index): + print("model {}".format(model)) + print( + "delegate setData: {}x{} => {}".format( + index.row(), index.column(), editor.text() + ) + ) + model.setData(index, editor.text()) + model.setData(index, float(editor.text()), Qt.UserRole) + + def emitCommitData(self): + print("imagedelegate emitting") + self.commitData.emit(self.sender()) + + +class WndFixTarget(QWidget): + prefixSelected = pyqtSignal(str) + dataFileLoaded = pyqtSignal(str) + prelocatedDataUpdated = pyqtSignal() + markersDeleted = pyqtSignal() + markerAdded = pyqtSignal(bool, list) # emits is_fiducial(boolean), [x, y, cx, cy] + selectedRowChanged = pyqtSignal(int) + moveFastStageRequest = pyqtSignal(float, float) + + def __init__(self, parent=None,data=None): + super(WndFixTarget, self).__init__(parent) + self._xtals_transformed = True + + self._current_row = None + + layout = QVBoxLayout() + + self.setLayout(layout) + frame = QWidget() + bl = QGridLayout() + frame.setLayout(bl) + + btnLd = QPushButton("Load Datafile") + btnLd.clicked.connect(lambda: self.load_file(None)) + bl.addWidget(btnLd, 0, 0) + + btnSv = QPushButton("Save Datafile") + btnSv.clicked.connect(lambda: self.save_file()) + bl.addWidget(btnSv, 0, 1) + + # but = QPushButton("Random Data") + # but.clicked.connect(self.generate_random_data) + # bl.addWidget(but, 4, 0) + # self._num_random_points = QSpinBox() + # self._num_random_points.setMinimum(2) + # self._num_random_points.setMaximum(10000) + # self._num_random_points.setValue(10) + # self._num_random_points.setSuffix(" points") + # bl.addWidget(self._num_random_points, 4, 1) + + btnDump = QPushButton("dump") + btnDump.clicked.connect(self.dump_data) + bl.addWidget(btnDump, 0, 2) + + self._cbType=cb=QComboBox() + #cb.addItem("C") + #cb.addItem("C++") + cb.addItems(["Fiducial", "FixTarget(12.5x12.5)", "FixTarget(23.0x23.0)", "FixTarget()", "Grid()"]) + + + #cb.currentIndexChanged.connect(self.selectionchange) + + bl.addWidget(cb, 1,0,1,1) + self._txtParam=param=QLineEdit() + bl.addWidget(param, 1, 1,1,1) + + self._btnAdd=btnAdd = QPushButton("add obj") + btnAdd.clicked.connect(self.add_obj) + bl.addWidget(btnAdd, 1, 2,1,1) + + + # but = QPushButton("Dump to console") + # but.clicked.connect(self.dump_data) + # bl.addWidget(but, 10, 0, 1, 1) + + but = QCheckBox("collect fiducials") + but.setChecked(False) + but.setToolTip("Collect or not the fiducial positions.") + self._collect_fiducials = False + but.stateChanged.connect(self.set_collect_fiducials) + bl.addWidget(but, 10, 0, 1, 1) + + + but = QCheckBox("draw crystal marks") + but.setChecked(False) + self._draw_crystal_marks = False + but.stateChanged.connect(self.set_draw_crystal_marks) + bl.addWidget(but, 10, 1, 1, 1) + + + but = QPushButton("Transform") + but.clicked.connect(self.transform_non_fiducials_in_model) + bl.addWidget(but, 20, 0, 2, 2) + + layout.addWidget(frame) + self.markersTable = QTableWidget() + self.markersTable.setSelectionMode(QAbstractItemView.SingleSelection) + self.markersTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.markersTable.setItemDelegate(MarkerDelegate(self)) + + # self.markersTable.horizontalHeader().setDefaultSectionSize(80) + self.markersTable.setColumnCount(5) + self.markersTable.setHorizontalHeaderLabels(("Fiducial?", "orig X", "orig Y", "X", "Y")) + hh=self.markersTable.horizontalHeader() + hh.setSectionResizeMode(0, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(1, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(2, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(3, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(4, QHeaderView.ResizeToContents) + hh.setSectionResizeMode(QHeaderView.Stretch) + + self.markersTable.itemSelectionChanged.connect(self.selection_changed) + + self.markersTable.setContextMenuPolicy(Qt.ActionsContextMenu) + + deleteRowAction = QAction("Delete this row", self) + deleteRowAction.triggered.connect(self.delete_selected) + self.markersTable.addAction(deleteRowAction) + + moveRequestAction = QAction("Move Here", self) + moveRequestAction.triggered.connect(self.request_stage_movement) + self.markersTable.addAction(moveRequestAction) + + layout.addWidget(self.markersTable, stretch=2) + self.connect_all_signals() + + #self._yamlFn=fn=os.path.expanduser('~/.config/PSI/SwissMX.yaml') + + #self._tree=tree=pg.DataTreeWidget(data=self._data) + self._data=data + self._tree=tree=pg.DataTreeWidget(data=data) + layout.addWidget(tree, stretch=2) + + def load_file(self, filename=None): + app = QApplication.instance() + #cfg = app._cfg + #logger = logging.getLogger("preloc.loadMarkers") + #def_folder = join(folders.pgroup_folder, "preloc_sheets") #ZAC: orig. code + #def_folder ='' + #data_folder = cfg.value("folders/last_prelocation_folder", def_folder) + data_folder='' + if filename is None: + filename, _ = QFileDialog.getOpenFileName(self,"Load yaml data file",None, + "yaml files (*.yaml);;json files (*.json);;pickle files (*.pkl);;text files (*.txt);;all files (*)",) + if not filename: # cancelled dialog + return + + ext=filename.rsplit('.',1)[1].lower() + if ext=='pkl': + with open(filename, 'rb') as f: + data=pickle.load(f) + if ext=='yaml': + with open(filename, 'r') as f: + data = yaml.load(f) + elif ext=='json': + with open(filename, 'r') as f: + data=json.load(f,object_hook=MyJsonDecoder) + print(data) + self._data=data + self._tree.setData(data) + return + + + try: + cfg.setValue("folders/last_prelocation_folder", QFileInfo(filename).canonicalPath()) + cfg.setValue("folders/last_prelocation_sheet", QFileInfo(filename).absolutePath()) + except TypeError as e: + pass + + self.clearMarkers() + + _log.info(f"loading prelocated coords from {filename}") + df = pd.read_csv(filename, comment="#", header=None, delim_whitespace=False, delimiter="[\t,;]") + + try: + prefix = QFileInfo(filename).baseName() + except: + prefix = filename.name + filename = prefix + + logger.info(f"prefix => {prefix}") + + gonio_coords_available = True + for data in df.as_matrix(): # FIXME FutureWarning: Method .as_matrix will be removed in a future version. Use .values instead. + row = self.markersTable.rowCount() + self.markersTable.setRowCount(row + 1) + original = np.copy(data) + data = list(data) + + if len(data) in [2, 3]: + gonio_coords_available = False + data.extend([0, 0]) + + if len(data) == 4: + data.insert(0, False) # fiducial flag + elif len(data) == 5: # fiducial already there, convert to bool + data[0] = bool(data[0]) + else: + QMessageBox.warning( + self, + "Wrong number of points in data file", + "I was expecting either 2, 3, 4 or 5 data points per line." + "\n\nFailed around a line with: {}".format(list(original)), + ) + + self.addSingleMarker(row, data) + + self._xtals_transformed = False + self.prefixSelected.emit(prefix) + + # only emit this signal if goniometer coordinates already read from file + if gonio_coords_available: + logger.debug(f"dataFileLoaded.emit => {filename}") + self.dataFileLoaded.emit(filename) + + def save_file(self, filename=None): + # filename = folders.get_file("prelocated-save.dat") + #data_folder = settings.value("folders/last_prelocation_folder") + data_folder='' + if filename is None: + filename, _ = QFileDialog.getSaveFileName(self,"Soad yaml data file",data_folder, + "yaml files (*.yaml);;json files (*.json);;pickle files (*.pkl);;text files (*.txt);;all files (*)",) + if not filename: + return + + #settings.setValue("folders/last_prelocation_folder", QFileInfo(filename).canonicalPath()) + #_log.info("Saving data in {}".format(filename)) + #data = self.get_data(as_numpy=True) + #df = pd.DataFrame(data) + #df.to_csv(filename, float_format="%.6f") + #import numpy as np + ext=filename.rsplit('.',1)[1].lower() + if ext=='pkl': + with open(filename, 'wb') as f: + pickle.dump(self._data, f) + elif ext=='yaml': + with open(filename, 'w') as f: + yaml.dump(self._data, f) + elif ext=='json': + with open(filename, 'w') as f: + json.dump(self._data, f,cls=MyJsonEncoder) + print(self._data) + + def add_obj(self): + _log.warning("TODO: IMPLEMENT") + + def delete_selected(self): + row = self._current_row + try: + row += 0 + except: + _log.warning("select a row first") + QMessageBox.warning(self,"Select a marker first","You must select a marker first before updating its goniometer position",) + return + self.markersTable.removeRow(row) + self.prelocatedDataUpdated.emit() + + def selection_changed(self): + row = self.markersTable.currentRow() + if row < 0: + return + self._current_row = row + self.selectedRowChanged.emit(row) + _log.debug("selection changed: current row {}".format(row)) + + def connect_all_signals(self): + self.prefixSelected.connect(lambda t: self._label_prefix.setText(t)) + self.dataFileLoaded.connect(lambda t: self._label_datafile.setText(t)) + + def markerItemChanged(self, item): + print(item.row(), item.column()) + + def set_fiducial_coords(self, camx, camy, gx, gy): + tbl = self.markersTable + row = self._current_row + try: + row += 0 + except: + _log.warning("select a row first") + QMessageBox.warning(self,"Select a marker first","You must select a marker first before updating its goniometer position",) + return + + origx = tbl.item(row, ORIGINAL_X).data(RoleOriginalCoord_X) + origy = tbl.item(row, ORIGINAL_Y).data(RoleOriginalCoord_Y) + + item = tbl.item(row, DATA_ITEM) + item.setData(RoleCameraCoord_X, camx) + item.setData(RoleCameraCoord_Y, camy) + item.setData(RoleGoniometerCoord_X, gx) + item.setData(RoleGoniometerCoord_Y, gy) + # item.setData(RoleOriginalCoord_X, origx) + # item.setData(RoleOriginalCoord_Y, origy) + # logger.debug(': [{}] = Original: {}, {} | Camera: {}, {} | Gonio: {}, {}'.format(1+row, origx, origy, camx, camy, gx, gy)) + logger.debug(f": [{1+row}] = Original: {origx}, {origy} | Camera: {camx}, {camy} | Gonio: {gx}, {gy}") + + tbl.item(row, GONIO_X).setData(Qt.DisplayRole, f"{gx:8.5f} mm\n{camx:8.1f} px") + tbl.item(row, GONIO_Y).setData(Qt.DisplayRole, f"{gy:8.5f} mm\n{camy:8.1f} px") + + # mark row as a fiducial + tbl.item(row, 0).setCheckState(Qt.Checked) + tbl.item(row, 0).setData(Qt.UserRole, True) + tbl.selectRow(row + 1) + self.prelocatedDataUpdated.emit() + + def set_selected_gonio_coords(self, xy): + tbl = self.markersTable + row = self._current_row + try: + row += 0 + except: + _log.warning("select a row first") + QMessageBox.warning(self,"Select a marker first","You must select a marker first before updating its goniometer position",) + return + + for n, v in enumerate(xy): + idx = 3 + n + tbl.setCurrentCell(row, idx) # gonio X + _log.debug("item: [{},{}] = {}".format(row, idx, v)) + item = tbl.currentItem() + item.setData(Qt.EditRole, "{:.3f}".format(v)) + item.setData(Qt.UserRole, v) + + # mark row as a fiducial + tbl.setCurrentCell(row, 0) + tbl.currentItem().setCheckState(Qt.Checked) + tbl.currentItem().setData(Qt.UserRole, True) + + def set_data_goniometer(self, row: int, xy): + tbl = self.markersTable + + row = self._current_row + try: + row += 0 + except: + _log.warning("select a row first") + QMessageBox.warning(self,"Select a marker first","You must select a marker first before updating its goniometer position",) + return + + for n, v in enumerate(xy): + idx = 3 + n + tbl.setCurrentCell(row, idx) # gonio X + item = tbl.currentItem() + item.setData(Qt.EditRole, "{:.3f}".format(v)) + item.setData(Qt.UserRole, v) + + @pyqtSlot(int) + def set_draw_crystal_marks(self, val): + logger.info(f"{'' if val else 'not '}drawing crystal markers") + self._draw_crystal_marks = val + + @pyqtSlot(int) + def set_collect_fiducials(self, val): + logger.info(f"{'' if val else 'not '}collecting fiducials") + self._collect_fiducials = val + + def get_collection_targets(self): + return self.get_data(fiducials=self._collect_fiducials) + + + def get_data(self, fiducials=True, crystals=True, as_numpy=False): + """return a list of tuples with all defined data + + [ + (is_fiducial(boolean), x, y, gonio_x, gonio_y),... + ] + """ + data = [] + item = self.markersTable.item + + for row in range(self.markersTable.rowCount()): + is_fiducial = Qt.Checked == item(row, 0).checkState() + ditem = item(row, DATA_ITEM) + + x = ditem.data(RoleGoniometerCoord_X) + y = ditem.data(RoleGoniometerCoord_Y) + cx = ditem.data(RoleCameraCoord_X) + cy = ditem.data(RoleCameraCoord_Y) + origx = ditem.data(RoleOriginalCoord_X) + origy = ditem.data(RoleOriginalCoord_Y) + + if is_fiducial and fiducials: + data.append([is_fiducial, x, y, cx, cy, origx, origy]) + if not is_fiducial and crystals: + data.append([is_fiducial, x, y, cx, cy, origx, origy]) + + if as_numpy: + data = np.asarray(data) + return data + + def get_original_coordinates(self, fiducials=True): + """return a numpy array with the original prelocated coordinates of the fiducial entries""" + data = self.get_data(fiducials=fiducials, crystals=not fiducials, as_numpy=True) + data = data[:, 5:] + zeros = np.zeros(data.shape) + zeros[:, 1] = 1 + return np.concatenate((data, zeros), axis=1) + + def get_goniometer_coordinates(self, fiducials=True): + """return a numpy array with the goniometer coordinates of the fiducial entries""" + data = self.get_data(fiducials=fiducials, crystals=not fiducials, as_numpy=True) + data = data[:, 1:3] + zeros = np.zeros(data.shape) + zeros[:, 1] = 1 + return np.concatenate((data, zeros), axis=1) + + def get_camera_coordinates(self, fiducials=True): + """return a numpy array with the goniometer coordinates of the fiducial entries""" + data = self.get_data(fiducials=fiducials, crystals=not fiducials, as_numpy=True) + data = data[:, 3:5] + zeros = np.zeros(data.shape) + zeros[:, 1] = 1 + return np.concatenate((data, zeros), axis=1) + + def dump_matrix(self, M): + scale, shear, angles, translate, perspective = tfs.decompose_matrix(M) + angles_deg = [a * 180 / np.pi for a in angles] + + print("Transformation matrix Aerotech => SwissMX") + print(M) + print((" scale {:9.4f} {:9.4f} {:9.4f}".format(*scale))) + print((" shear {:9.4f} {:9.4f} {:9.4f}".format(*shear))) + print((" angles rad {:9.4f} {:9.4f} {:9.4f}".format(*angles))) + print((" angles deg {:9.4f} {:9.4f} {:9.4f}".format(*angles_deg))) + print((" translate {:9.4f} {:9.4f} {:9.4f}".format(*translate))) + print(("perspective {:9.4f} {:9.4f} {:9.4f}".format(*perspective))) + + def transform_non_fiducials_in_model(self): + forg = self.get_original_coordinates(fiducials=True) + fgon = self.get_goniometer_coordinates(fiducials=True) + fcam = self.get_camera_coordinates(fiducials=True) + + gmat = sort_cmass_3d(fgon) + omat = sort_cmass_3d(forg) + + try: + M_org2gon = tfs.superimposition_matrix(omat.T, gmat.T, scale=True) + M_org2cam = tfs.superimposition_matrix(forg.T, fcam.T, scale=True) + except: + QMessageBox.warning(self.parent(), title="failed to find superimposition matrix", text="Failed to find superimposition matrix.\n\tPlease try again.") + return + + # scale, shear, angles, translate, perspective = tfs.decompose_matrix(M_org2gon) + + org_data = self.get_original_coordinates(fiducials=False) + + gon_data = np.dot(M_org2gon, org_data.T) + cam_data = np.dot(M_org2cam, org_data.T) + + tbl = self.markersTable + num_fiducials = forg.shape[0] + + gon_data = (gon_data.T)[:, 0:2] # only X,Y matters + cam_data = (cam_data.T)[:, 0:2] # only X,Y matters + combined = np.concatenate((gon_data, cam_data), axis=1) + + item = self.markersTable.item # function alias + for row, data in enumerate( + combined, num_fiducials + ): # enumeration starts at *num_fiducials* + gx, gy, cx, cy = data + ditem = item(row, DATA_ITEM) + ditem.setData(RoleCameraCoord_X, cx) + ditem.setData(RoleCameraCoord_Y, cy) + ditem.setData(RoleGoniometerCoord_X, gx) + ditem.setData(RoleGoniometerCoord_Y, gy) + item(row, GONIO_X).setData(Qt.DisplayRole, f"{gx:8.5f} mm\n{cx:8.1f} px") + item(row, GONIO_Y).setData(Qt.DisplayRole, f"{gy:8.5f} mm\n{cy:8.1f} px") + self._xtals_transformed = True + self.prelocatedDataUpdated.emit() + + def request_stage_movement(self): + logger = logging.getLogger("preloc.move_stage") + row = self._current_row + x = self.markersTable.item(row, DATA_ITEM).data(RoleGoniometerCoord_X) + y = self.markersTable.item(row, DATA_ITEM).data(RoleGoniometerCoord_Y) + logger.info(f"request move gonio to {x:.3f}, {y:.3f} mm") + self.moveFastStageRequest.emit(x, y) + + def dump_numpy(self): + R = tfs.random_rotation_matrix(np.random.random(3)) + d = self.get_goniometer_coordinates() + print("dumping") + print(d) + dR = np.dot(R, d) + print(dR.T) + + def get_fiducials(self, as_numpy=False): + return self.get_data(crystals=False, as_numpy=as_numpy) + + def get_crystals(self, as_numpy=False): + return self.get_data(fiducials=False, as_numpy=as_numpy) + + def dump_data(self): + print(self._data) + #for ref, x, y, cx, cy, ox, oy in self.get_data(): + # print(f"fiducial:{ref} [{x}, {y}, {cx}, {cy}]") + + def append_data(self, data): + """append data (a list of values) to the model + + len(data) == 2 => (X, Y) from prelocated coordinate + len(data) == 4 => (X, Y, GX, GY) plus gonio coordinates + len(data) == 5 => (fiducial?, X, Y, GX, GY) plus fiducial + """ + row = self.markersTable.rowCount() + self.markersTable.setRowCount(row + 1) + + data = list(data) + if len(data) == 2: + data.extend([0, 0]) + if len(data) == 4: + data.insert(0, False) # fiducial flag + self.addSingleMarker(row, data) + self.prelocatedDataUpdated.emit() + + def clearMarkers(self): + self.markersTable.setRowCount(0) + self.markersDeleted.emit() + + def generate_random_data(self): + import io + + data = io.StringIO() + npoints = self._num_random_points.value() + for n in range(npoints): + x, y, a, b = ( + np.random.randint(0, 2000), + np.random.randint(0, 2000), + np.random.randint(-4000, 4000) / 1000., + np.random.randint(-4000, 4000) / 1000., + ) + data.write("{}\t{}\t{}\t{}\n".format(x, y, a, b)) + data.seek(0) + data.name = "random.csv" + self.loadMarkers(data) + + + + + def addSingleMarker(self, row, marker): + is_fiducial, origx, origy, gx, gy = marker + _log.debug(f": [{1+row}] | Original: {origx}, {origy} | Gonio: {gx}, {gy}") + item0 = QTableWidgetItem() + item0.setData(Qt.UserRole, is_fiducial) + item0.setFlags(item0.flags() & ~Qt.ItemIsEditable) + item0.setCheckState(Qt.Checked if is_fiducial else Qt.Unchecked) + item0.setTextAlignment(Qt.AlignCenter) + + item1 = QTableWidgetItem("{:.1f}".format(origx)) + item1.setTextAlignment(Qt.AlignRight) + + item2 = QTableWidgetItem("{:.1f}".format(origy)) + item2.setTextAlignment(Qt.AlignRight) + + item3 = QTableWidgetItem("{:.3f} mm".format(gx)) + item3.setFlags(item3.flags() & ~Qt.ItemIsEditable) + item3.setTextAlignment(Qt.AlignRight) + + item4 = QTableWidgetItem("{:.3f} mm".format(gy)) + item4.setFlags(item4.flags() & ~Qt.ItemIsEditable) + item4.setTextAlignment(Qt.AlignRight) + + self.markersTable.setItem(row, 0, item0) + self.markersTable.setItem(row, ORIGINAL_X, item1) + self.markersTable.setItem(row, ORIGINAL_Y, item2) + self.markersTable.setItem(row, GONIO_X, item3) + self.markersTable.setItem(row, GONIO_Y, item4) + self.markerAdded.emit(is_fiducial, [origx, origy, gx, gy]) + + item = self.markersTable.item(row, DATA_ITEM) + item.setData(RoleCameraCoord_X, origx) # initially set to original + item.setData(RoleCameraCoord_Y, origy) # initially set to original + item.setData(RoleGoniometerCoord_X, gx) + item.setData(RoleGoniometerCoord_Y, gy) + item.setData(RoleOriginalCoord_X, origx) + item.setData(RoleOriginalCoord_Y, origy) + +""" +Signals QTableWidget +void cellActivated(int row, int column) +void cellChanged(int row, int column) +void cellClicked(int row, int column) +void cellDoubleClicked(int row, int column) +void cellEntered(int row, int column) +void cellPressed(int row, int column) +void currentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn) +void currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous) +void itemActivated(QTableWidgetItem *item) +void itemChanged(QTableWidgetItem *item) +void itemClicked(QTableWidgetItem *item) +void itemDoubleClicked(QTableWidgetItem *item) +void itemEntered(QTableWidgetItem *item) +void itemPressed(QTableWidgetItem *item) +void itemSelectionChanged() +""" + +if __name__ == "__main__": + import sys + import pyqtUsrObj as UsrGO + + class Monster(yaml.YAMLObject): + yaml_tag = u'!Monster' + def __init__(self, name, hp, ac, attacks): + self.name = name + self.hp = hp + self.ac = ac + self.attacks = attacks + def __repr__(self): + return "%s(name=%r, hp=%r, ac=%r, attacks=%r)" % ( + self.__class__.__name__, self.name, self.hp, self.ac, self.attacks) + + + class MainWindow(QMainWindow): + def __init__(self, parent=None): + super(MainWindow, self).__init__(parent) + self.move(100,100) + self.resize(500,800) + self.centralWidget=QWidget() + self.wndFixTrg=WndFixTarget(parent=self) + + self.setCentralWidget(self.centralWidget) + mainLayout=QVBoxLayout() + mainLayout.addWidget(self.wndFixTrg) + self.centralWidget.setLayout(mainLayout) + + + app = QApplication(sys.argv) + mainWin = MainWindow() + + t=3 + if t==0: + data=[{'name':'John Doe', 'occupation':'gardener'}, {'name':'Lucy Black', 'occupation':'teacher'}] + elif t==1: + data ={ + 'name': 'John Doe', 'occupation': 'gardener', + 'A':(1,2,3), + 'B':[1,2,3], + 'C':{1,2,3}, + 'D': + {'1':(1,2,3), + '2':'df', + '3':'dddd'}, + '4':np.array(((1,2,3),(4,5,6))), + '5':np.array(range(40*2)).reshape(-1,2) + } + elif t==2: + data = { + 'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { + 'x': 1, + 'y': 2, + 'z': 'three' + }, + 'an array': np.random.randint(10, size=(40,10)), + #'a traceback': some_func1(), + #'a function': some_func1, + #'a class': pg.DataTreeWidget, + } + elif t==3: + data=[ + UsrGO.Grid(pos=(120.0, -100.0), size=(1000.0, 500.0), cnt=(10, 5), ficucialScale=2), + Monster(name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT']) + ] + mainWin.wndFixTrg._data=data + mainWin.wndFixTrg._tree.setData(data) + mainWin.show() + sys.exit(app.exec_()) diff --git a/camera.py b/camera.py index 09e46dc..f7385ff 100755 --- a/camera.py +++ b/camera.py @@ -248,9 +248,9 @@ class epics_cam(object): hr=h//4 imgSeq[:,0:hr,0:wr]+=np.random.randint(0,100,(t,hr,wr),dtype=np.uint16) elif mode==1: - import glob,PIL.Image + import os,glob,PIL.Image #path='/home/zamofing_t/Documents/prj/SwissFEL/epics_ioc_modules/ESB_MX/python/SwissMX/simCamImg/*.png' - path='simCamImg/*.png' + path=os.path.join(os.path.dirname(__file__),'simCamImg/*.png') _log.info('generate simulation images:{}...'.format(path)) glb=glob.glob(path) img = PIL.Image.open(glb[0]) diff --git a/pyqtUsrObj.py b/pyqtUsrObj.py index c468014..eb521ff 100644 --- a/pyqtUsrObj.py +++ b/pyqtUsrObj.py @@ -138,6 +138,27 @@ class Marker(pg.ROI): # p.drawRect(-w / 2, -h / 2, w, h) # # p.drawText(-w, -h, '{:.0f}x{:.0f}'.format(*self._size)) +class Fiducial(pg.ROI): + def __init__(self, pos, size, xyz, **kargs): + pg.ROI.__init__(self, pos, size, **kargs) + self._xyz=xyz + + def paint(self, p, *args): + #pg.ROI.paint(self, p, *args) + r=QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.currentPen) + p.translate(r.left(), r.top()) + p.scale(.01*r.width(), .01*r.height()) # -> values x,y 0 to 100 + p.drawRect(0, 0, 100, 100) + p.setPen(pg.mkPen(width=1, color=[255, 0, 0])) + p.drawLine(pg.Point(50,10), pg.Point(50, 90)) + p.drawLine(pg.Point(10,50), pg.Point(90, 50)) + tr=p.transform() + tr.setMatrix(tr.m11(), tr.m12(), tr.m13(), tr.m21(), -tr.m22(), tr.m23(), tr.m31(), tr.m32(), tr.m33()) + p.setTransform(tr) + + class Grid(pg.ROI): '''a grid''' @@ -200,6 +221,25 @@ class Grid(pg.ROI): #p.drawEllipse(0, 0, 1, 1) #p.drawRect(0, 0, 1, 1) + def __repr__(self): + s=f'{self.__class__.__name__}:(pos:{tuple(self.pos())}, size:{tuple(self.size())}, cnt:{self._cnt}, ficucialScale:{self._fidScl}}}' + return s +import yaml +def yaml_repr_Grid(dumper, data): + #s=f'{{"pos":{tuple(data.pos())}, "size":{tuple(data.size())}, "cnt":{data._cnt}, "ficucialScale":{data._fidScl}}}' + #return dumper.represent_scalar(u'!Grid', s) + m={'pos':tuple(data.pos()),'size':tuple(data.size()),'cnt':data._cnt, 'ficucialScale':data._fidScl} + return dumper.represent_mapping(u'!Grid', m) +yaml.add_representer(Grid, yaml_repr_Grid) + +def yaml_cnstr_Grid(loader, node): + #value = loader.construct_scalar(node) + #kwargs=eval(value) + #return Grid(**kwargs) + m = loader.construct_mapping(node) + return Grid(**m) +yaml.add_constructor(u'!Grid', yaml_cnstr_Grid) + class Path(pg.ROI): ''' @@ -524,6 +564,10 @@ if __name__=='__main__': vi=FixTargetFrame((400,-200),(400,400),tpl='12.5x12.5') vb.addItem(vi) + vi=Fiducial((0,200),(40,40),(1,2,3)) + vb.addItem(vi) + + childTree(vb) w.scene().sigMouseClicked.connect(mouse_click_event) diff --git a/swissmx.py b/swissmx.py index 3311e8d..74b2bc8 100755 --- a/swissmx.py +++ b/swissmx.py @@ -69,6 +69,8 @@ TASK_PRELOCATED = "prelocated" TASK_HELICAL = "helical" TASK_EMBL = "embl" ts.log('Import part 2/8:') + +import ModuleFixTarget import PrelocatedCoordinatesModel # ZAC: orig. code from EmblModule import EmblWidget #ZAC: orig. code from HelicalTable import HelicalTableWidget #ZAC: orig. code @@ -644,6 +646,19 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): camera.epics_cam.set_fiducial(pic, 255) self._goImg.setImage(pic) + def new_frame_sim_cb(self, **kwargs): + app=QApplication.instance() + sim=app._camera._sim + imgSeq=sim['imgSeq'] + idx=sim['imgIdx'] + sim['imgIdx']=(idx+1)%imgSeq.shape[0] + # _log.info('simulated idx:{}'.format(idx)) + pic=imgSeq[idx] + self._goImg.setImage(pic) + + delay=500 # ms -> 2fps + QtCore.QTimer.singleShot(delay, self.new_frame_sim_cb) + def load_stylesheet(self): with open("swissmx.css", "r") as sheet: self.setStyleSheet(sheet.read()) @@ -673,11 +688,11 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): toolbox = QToolBox() layout.addWidget(toolbox) - self.build_faststage_group(toolbox) + self.build_group_faststage(toolbox) # self.build_slits_group(toolbox) - self.build_collimator_group(toolbox) - self.build_posttube_group(toolbox) - self.build_xeye_group(toolbox) + self.build_group_collimator(toolbox) + self.build_group_posttube(toolbox) + self.build_group_xeye(toolbox) #self.build_cryo_group(toolbox) # monitor all axis for an axis fault @@ -801,7 +816,7 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): msg = "" self._message_critical_fault.setText(msg) - def build_faststage_group(self, toolbox): + def build_group_faststage(self, toolbox): qutilities.add_item_to_toolbox(toolbox,"Fast Stage", widget_list=[ # self.get_tweaker('SAR-EXPMX:MOT_BLGT', alias='backlight', label='backlight'), @@ -813,7 +828,7 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): ], ) - def build_collimator_group(self, toolbox): + def build_group_collimator(self, toolbox): qutilities.add_item_to_toolbox(toolbox,"Collimator", widget_list=[ self.get_tweaker("SARES30-ESBMX1", alias="colli_x", label="colli X", mtype="smaract_motor",), @@ -821,7 +836,7 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): ], ) - def build_posttube_group(self, toolbox): + def build_group_posttube(self, toolbox): widgets = [ self.get_tweaker("SARES30-ESBMX4", alias="tube_usx", label="upstream X", mtype="smaract_motor",), self.get_tweaker("SARES30-ESBMX6", alias="tube_usy", label="upstream Y", mtype="smaract_motor",), @@ -851,7 +866,7 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): block.layout().addStretch() toolbox.addItem(block, label) - def build_xeye_group(self, toolbox): + def build_group_xeye(self, toolbox): qutilities.add_item_to_toolbox( toolbox, "X-Ray Eye", @@ -1509,12 +1524,33 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): exp_tab.layout().addStretch() # DAQ Methods Tabs + self.build_tab_module_fix_target() self._OLD_build_daq_methods_grid_tab() self._OLD_build_daq_methods_prelocated_tab() #ZAC: orig. code self._OLD_create_helical_widgets() #ZAC: orig. code self._OLD_build_daq_methods_embl_tab() #self._OLD_build_sample_selection_tab() + def build_tab_module_fix_target(self): + #tab = self._tab_daq_method_prelocated + data=self._goTracked['objLst'] + + self._moduleFixTarget =mft = ModuleFixTarget.WndFixTarget(self,data) + tab = self._tabs_daq_methods.insertTab(0,mft,'Fix Target') + mft._btnAdd.clicked.connect(self.module_fix_target_add_obj) + + + #tab.layout().addWidget(mft) + mft.prefixSelected.connect(lambda prefix: self._le_prefix.setText(prefix)) + mft.dataFileLoaded.connect(self._OLD_daq_method_prelocated_update_markers) + mft.prelocatedDataUpdated.connect(self._OLD_daq_method_prelocated_update_markers) + mft.markersDeleted.connect(self._OLD_daq_method_prelocated_remove_markers) + mft.moveFastStageRequest.connect(self._OLD_move_fast_stage) + self.fiducialPositionSelected.connect(self._OLD_daq_method_prelocated_set_fiducial) + self.appendPrelocatedPosition.connect(self._OLD_daq_method_prelocated_append_data) + #self._preloc_inspect_area = QPlainTextEdit() + #tab.layout().addWidget(self._preloc_inspect_area) + def switch_task(self, index=0): stack = self._centerpiece_stack @@ -1573,8 +1609,42 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): del app._raw_opt_ctr return + def module_fix_target_add_obj(self,*args,**kwargs): + mft=self._moduleFixTarget + fx=self.tweakers["fast_x"].get_rbv() + fy=self.tweakers["fast_y"].get_rbv() + #cz=self.tweakers["fast_y"].get_rbv() + app=QApplication.instance() + geo=app._geometry + oc=geo._opt_ctr + idx=mft._cbType.currentIndex() + param=mft._txtParam.text() -# **************** OBSOLETE AND/OR OLD STUFF **************** + #cb.addItems(["Fiducial", "FixTarget(12.5x12.5)", "FixTarget(23.0x23.0)", "FixTarget()", "Grid()"]) + + if idx==0: + go=UsrGO.Fiducial((120, -100), (200, 150),(1,2,3)) + elif idx==1: + v=geo.pos2pix((12.5, 0)) + l=np.linalg.norm(v) + go=UsrGO.FixTargetFrame(-oc, (l, l), tpl='12.5x12.5') + elif idx==2: + v=geo.pos2pix((23, 0)) + l=np.linalg.norm(v) + go=UsrGO.FixTargetFrame(-oc, (l, l), tpl='23.0x23.0') + elif idx==3: + go=UsrGO.FixTargetFrame((120, -100), (200, 150), tpl='test') + elif idx==4: + go=UsrGO.Grid((120, -100), (200, 150), (30, 22), 2) + else: + + _log.error('set xstep 0..2 for tests') + self.vb.addItem(go) + obj=self._goTracked['objLst'] + obj.append(go) + mft._tree.setData(obj) + + # **************** OBSOLETE AND/OR OLD STUFF **************** # functions are prefixed with _OLD_ @@ -1677,19 +1747,6 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): app=QApplication.instance() app._camera.pause() - def _OLD_new_frame_sim_cb(self, **kwargs): - app=QApplication.instance() - sim=app._camera._sim - imgSeq=sim['imgSeq'] - idx=sim['imgIdx'] - sim['imgIdx']=(idx+1)%imgSeq.shape[0] - # _log.info('simulated idx:{}'.format(idx)) - pic=imgSeq[idx] - self._goImg.setImage(pic) - - delay=500 # ms -> 2fps - QtCore.QTimer.singleShot(delay, self.new_frame_sim_cb) - def _OLD_init_settings_tracker(self): app=QApplication.instance() cfg=app._cfg