diff --git a/geometry.py b/geometry.py index 176c3cd..11ffedc 100644 --- a/geometry.py +++ b/geometry.py @@ -7,6 +7,10 @@ # *-----------------------------------------------------------------------* ''' coordinate systems, optical center, xray axis, pixel sizes etc. + +ppm= pixel per mm +update_pix2pos + ''' import logging import numpy as np @@ -27,41 +31,84 @@ class geometry: # this coordinate represents also the origin of other coordinates pass - def zoom2pixsz(self,zoom): - # this returns the pixel size at a given zoom level + def interp_zoom(self,zoom): + # this calculates the interpoated pix2pos coordinate transformation matrix # the returned value is a 2x2 matrix: - # [pxx pxy] - # [pyx pyy] which is interpolated out of a lookup table - # - # [pxx pxy] [nx] - # [pyx pyy]*[ny] results in a vector in meter of a vector [nx,ny] pixels in x and y direction - pass + # [a b] [mx] [px] + # [ ]*[ ]=[ ] + # [c d] [my] [py] - def set_zoom2pixsz(self,meas): + K,AA=self._lut_pix2pos + pix2pos=self._pix2pos=np.asmatrix(np.ndarray((2,2),np.float)) + pix2pos[0,0]=np.interp(zoom,K,AA[:,0,0]) + pix2pos[0,1]=np.interp(zoom,K,AA[:,0,1]) + pix2pos[1,0]=np.interp(zoom,K,AA[:,1,0]) + pix2pos[1,1]=np.interp(zoom,K,AA[:,1,1]) + _log.debug('interpolation array:{}'.format(tuple(pix2pos.ravel()))) + + def update_pix2pos(self,meas): #calculates _lut_z2p out of measurements # the _lut_z2p is dictionaty a lookuptable # zoom {1,200,400,600,800,1000} #[pxx pxy] #[pyx pyy] - self._lut_z2p=_lut_z2p={ - 'zoom': np.array((1,200,400,600,800,1000),dtype=np.float32), - 'pixsz': np.array( - #((pxx,pxy),(pyx,pyy)), # zoom n - ((( 1,0),(0, 1)), # zoom 1 - (( 2,0),(0, 2)), # zoom 200 - (( 4,0),(0, 4)), # zoom 400 - (( 6,0),(0, 6)), # zoom 600 - (( 8,0),(0, 8)), # zoom 800 - ((10,0),(0,10))), # zoom 1000 - dtype=np.float32)} + # + # [a b] [mx] [px] + # [ ]*[ ]=[ ] + # [c d] [my] [py] + # + # [a*mx1 b*my1] [px1] + # [ ... ... ] [...] + # [a*mxn b*myn] =[pxn] + # [c*mx1 d*my1] [py1] + # [ ... ... ] [...] + # [c*mxn d*myn] [pyn] + # + # [mx1 my1 0 0 ] [a] [px1] + # [... .. 0 0 ] [b] [...] + # [mxn myn 0 0 ]*[c]=[pxn] + # [ 0 0 mx1 my1] [d] [py1] + # [ 0 0 ... .. ] [...] + # [ 0 0 mxn myn] [pyn] + # + # A *aa = y - n=len(meas) - zoom =np.ndarray(shape=n,dtype=np.float32) - pixsz=np.ndarray(shape=(n,2,2),dtype=np.float32) + # https://de.wikipedia.org/wiki/Methode_der_kleinsten_Quadrate - for i,(k,v) in enumerate(meas): - pass - self._lut_z2p={ 'zoom': zoom, 'pixsz': pixsz} + # r= A*aa-y + # mit aa=(A.T*A)^-1*A.T*y + + # A=np.zeros((d.shape[0]*2,d.shape[1])) + # A[:d.shape[1]-1,:2]=A[d.shape[1]-1:,2:]=d[:,:2] + # + # y=d[:,2:].T.ravel() + # y=np.asmatrix(y).T + # + # A=np.asmatrix(A) + # aa=(A.T*A).I*A.T*y + # a,b,c,d=aa + # a,b,c,d=np.asarray(aa).ravel() + # + # aa=aa.reshape(2,-1) + AA=np.ndarray((len(meas),2,2),np.float) + K=np.array(tuple(meas.keys()),np.float) + self._lut_pix2pos=(K,AA) + for i,(k,v) in enumerate(meas.items()): + m=np.array(v) # measurements + d=m[1:]-m[0] # distances + + A=np.zeros((d.shape[0]*2,d.shape[1]),np.float) + A[:d.shape[1]-1,:2]=A[d.shape[1]-1:,2:]=d[:,:2] + + y=d[:,2:].T.ravel() + y=np.asmatrix(y).T + A=np.asmatrix(A) + + aa=(A.T*A).I*A.T*y + AA[i]=aa.reshape(2,-1) + #bx_coefs = np.polyfit(nbx[0], nbx[1], 3) + #np.poly1d(bx_coefs) + _log.debug('least square data:\nK:{}\nAA:{}'.format(K,AA)) def autofocus(self): # cam camera object @@ -75,12 +122,16 @@ class geometry: def pix2pos(self,p,zoom=None): # returns the position m(x,y) in meter relative to the optical center at a given zoom level of the pixel p(x,y) # if zoom=None, the last zoom value is used - pass + if zoom is not None: + self.interp_zoom(zoom) + return np.asarray(self._pix2pos*np.mat(p).T).ravel() def pos2pix(self,p,zoom=None): # returns the pixel p(x,y) of the position m(x,y) in meter relative to the optical center at a given zoom level # if zoom=None, the last zoom value is used - pass + if zoom is not None: + self.interp_zoom(zoom) + return np.asarray(self._pix2pos.I*np.mat(p).T).ravel() def optctr2xray(self): # returns the vector m(x,y) of the optical center to the xray @@ -130,16 +181,48 @@ if __name__ == "__main__": 1000:{'x': (+0.1 , ( 121, 632),(1044, 592)), 'y': (+0.08 , ( 593, 883),( 570, 145))}, } + measure={ + 1: [(-3.0 , -7.6 , 116.99110193046, 632.5463827525), + (-2.0 , -7.6 , 934.07501517856, 600.7926167715), + (-2.0 , -7.1 , 916.54131238102, 191.0366615002), + (-3.0 , -7.1 , 103.74668003329, 226.2150231456)], + 200: [(-3.1 , -7.3 , 113.66321353086, 824.9041423107), + (-2.3 , -7.3 , 1065.97386092697, 792.2851118419), + (-2.3 , -6.7 , 1033.68452410347, 74.0336610693), + (-3.1 , -6.7 , 84.62681572700, 116.6832971512)], + 400: [(-3.4 , -6.7 , 155.00053674203, 601.3838942136), + (-3.0 , -6.7 , 957.95919656052, 573.0827012272), + (-3.2 , -6.5 , 541.08684037200, 187.9171307943), + (-3.2 , -6.8 , 564.32152887203, 789.1146957326)], + 600: [(-3.3 , -6.8 , 328.27244399903, 509.5061192017), + (-3.1 , -6.8 , 992.78996735279, 488.0323963092), + (-3.2 , -6.9 , 672.03111567934, 832.4122409755), + (-3.2 , -6.7 , 645.70960116180, 164.2534779331)], + 800: [(-3.2 , -6.7 , 639.52253576449, 53.4455632943), + (-3.2 , -6.85, 671.47023245203, 882.6335091391), + (-3.3 , -6.75, 105.12470026379, 361.3051859197), + (-3.1 , -6.75, 1195.96864609255, 313.1068618673)], + 1000:[(-3.25, -6.75, 195.05641095116, 353.3492286375), + (-3.15, -6.75, 1117.27204644084, 314.9636405871), + (-3.2 , -6.8 , 675.10991143017, 790.3040145281), + (-3.2 , -6.72, 638.98580653116, 59.3803912957)]} + + import numpy as np + np.set_printoptions(suppress=True) + np.set_printoptions(linewidth=196) obj=geometry() - obj.calc_zoom2pixsz(measure) - - - - - - - + obj.update_pix2pos(measure) + + obj.interp_zoom(1) + obj.interp_zoom(200) + obj.interp_zoom(100) + + + + print(np.asarray(obj._pix2pos*np.mat((1,0)).T).ravel()) + print(obj._pix2pos*np.mat((0,1)).T) + print(obj._pix2pos*np.mat((2,.1)).T) diff --git a/swissmx.py b/swissmx.py index 57c27cf..8ee4f7e 100755 --- a/swissmx.py +++ b/swissmx.py @@ -97,7 +97,7 @@ from PyQt5.QtWidgets import ( QSplashScreen, QTextBrowser, QToolBox, QVBoxLayout, QWidget,) from PyQt5.uic import loadUiType -from CustomROI import BeamMark +import CustomROI as CstROI #from CustomROI import BeamMark, Grid, CrystalCircle #ZAC: orig. code #from GenericDialog import GenericDialog #ZAC: orig. code #from dialogs.PreferencesDialog import PreferencesDialog #ZAC: orig. code @@ -119,7 +119,7 @@ from app_config import settings, appsconf, option, toggle_option, simulated import epics from epics.ca import pend_event -import camera,backlight,zoom,illumination +import camera,backlight,zoom,illumination,geometry #_URL = "http://PC12288:8080" @@ -388,7 +388,7 @@ class Main(QMainWindow, Ui_MainWindow): def add_beam_marker(self): w, h = settings.value(BEAM_SIZE) - self._beammark = BeamMark([100, 100], (int(w), int(h)), parent=self) + self._beammark = CstROI.BeamMark([100, 100], (int(w), int(h)), parent=self) self.vb.addItem(self._beammark) def camera_pause_toggle(self): @@ -416,7 +416,7 @@ class Main(QMainWindow, Ui_MainWindow): else: sz=self.update_size() pic=kwargs['value'].reshape(sz[::-1]) - _log.debug('new_frame_pv_cb count {}'.format(kwargs['count'])) + #_log.debug('new_frame_pv_cb count {}'.format(kwargs['count'])) if pic.dtype==np.int16: pic.dtype=np.uint16 camera.epics_cam.set_fiducial(pic, 255) @@ -798,7 +798,8 @@ class Main(QMainWindow, Ui_MainWindow): grp.layout().addWidget(self._ppm_feature_size_spinbox, 0, 1) self._ppm_calibration = but = QPushButton("Start calibration") but.setCheckable(True) - but.clicked.connect(lambda x: self.update_ppm_fitters()) + #but.clicked.connect(lambda x: self.update_ppm_fitters()) + but.clicked.connect(self.update_ppm_fitters) grp.layout().addWidget(but, 1, 0, 1, 2) help = QTextBrowser() grp.layout().addWidget(help, 2, 0, 5, 2) @@ -806,9 +807,11 @@ class Main(QMainWindow, Ui_MainWindow): """

Pixel/MM Setup

    -
  1. choose: feature size
  2. press: Start calibration
  3. -
  4. for each zoom level, CTRL-Click (left mouse button) 2 locations which are feature-size apart
  5. +
  6. for each zoom level: +
  7. choose a good fiducial mark +
  8. move the stage x/y at min. 3 different locations +
  9. CTRL-Click (left mouse button) on the fiducial mark
  10. un-click: Start calibration
@@ -960,7 +963,18 @@ class Main(QMainWindow, Ui_MainWindow): self.beamx_fitter = np.poly1d(bx_coefs) self.beamy_fitter = np.poly1d(by_coefs) - def update_ppm_fitters(self, calib=None): + def update_ppm_fitters(self, calib): + app=QApplication.instance() + if calib: + _log.info("received new pix2pos calibration") + app._ppmRaw=dict() + else: + app._geometry.update_pix2pos(app._ppmRaw) + del app._ppmRaw + return + + + if calib is not None: _log.info("received new calibration for PPM") self._zoom_to_ppm = calib @@ -1010,17 +1024,17 @@ class Main(QMainWindow, Ui_MainWindow): self.pixelsPerMillimeter.emit(ppm) self.update_beam_marker(value) - def getZoom(self): - return qoptic_zoom.get_position() +# def getZoom(self): +# return qoptic_zoom.get_position() - def getPpm(self): - ppm_fitter = None - try: - ppm_fitter = self.ppm_fitter(self.getZoom()) - except Exception as e: - _log.error("garbage in getPpm()") - traceback.print_exc() - return ppm_fitter +# def getPpm(self): +# ppm_fitter = None +# try: +# ppm_fitter = self.ppm_fitter(self.getZoom()) +# except Exception as e: +# _log.error("garbage in getPpm()") +# traceback.print_exc() +# return ppm_fitter def append_to_beam_markers(self, x, y, zoom): self._beam_markers[zoom] = (x, y) @@ -1073,7 +1087,7 @@ class Main(QMainWindow, Ui_MainWindow): try: ppm = self.ppm_fitter(z) except: - ppm = 1 + ppm = 1E3 x, y = xy.x(), xy.y() try: bx, by = self.get_beam_mark_on_camera_xy() @@ -1103,15 +1117,48 @@ class Main(QMainWindow, Ui_MainWindow): ) ) - def mouse_click_event(self, event): + def mouse_click_event_ppm(self, event): app=QApplication.instance() - fx_motor, fy_motor, bx_motor, bz_motor, omega_motor = self.get_gonio_tweakers() + zoom = app._zoom.get() + fast_x=self.tweakers["fast_x"];fast_y=self.tweakers["fast_y"] + try: + raw=app._ppmRaw[zoom] + except (AttributeError,KeyError) as e: + if type(e) is AttributeError: + _log.info("") + msg="SwissMX *POST-SAMPLE-TUBE* configuration is incomplete!!!" + QMessageBox.warning(self, "pix2pos calibration", "Press 'Start calibration' button first") + return + app._ppmRaw=dict() + raw=app._ppmRaw[zoom]=list() + #event.screenPos() #position in pixel on the screen + #event.pos() #position in pixel on the widget + #event.scenePos() # position in user coordinates of the widget + #img.mapFromScene(scenePos) #pixel position of the image at scenePos user coordinates + #mousePos=event.pos() #position in pixel on the widget + #scenePos=event.scenePos() + #imgPos = self.img.mapFromScene(scenePos) + #_log.debug('mouse_pos:{} scene_pos:{} imgPos:{}'.format(tuple(mousePos),tuple(scenePos),(imgPos.x(),imgPos.y()))) + #_log.debug('fast stage:{} {}'.format(fast_x.get_position(),fast_y.get_position())) + imgPos = self.img.mapFromScene(event.scenePos()) + fx=fast_x.get_position();fy=fast_y.get_position() + x=imgPos.x();y=imgPos.y() + _log.debug('append calib: zoom:{} stage_x_y:{}/{} cam_pix_x_y:{}/{}'.format(zoom,fx,fy,x,y)) + raw.append((fx,fy,x,y)) + + def mouse_click_event(self, event): _log.debug("{}".format(event)) _log.debug(" pos {}".format(event.pos())) _log.debug("screen pos {}".format(event.screenPos())) _log.debug("scene pos {}".format(event.scenePos())) task = self.active_task() + if task==TASK_SETUP_PPM_CALIBRATION: + self.mouse_click_event_ppm(event) + return + + app=QApplication.instance() + fx_motor, fy_motor, bx_motor, bz_motor, omega_motor = self.get_gonio_tweakers() if event.button() == Qt.RightButton: print_transform(self.vb.childTransform()) @@ -1124,7 +1171,7 @@ class Main(QMainWindow, Ui_MainWindow): try: ppm = self.ppm_fitter(zoom_level) except: - ppm = 1 # 0: do not move if we don't know ppm + ppm = 1E3 dblclick = event.double() shft = Qt.ShiftModifier & event.modifiers() ctrl = Qt.ControlModifier & event.modifiers() @@ -1925,7 +1972,7 @@ class Main(QMainWindow, Ui_MainWindow): ystep = self._sb_grid_y_step.value() xoffset = self._sb_grid_x_offset.value() yoffset = self._sb_grid_y_offset.value() - grid = Grid( x_step=xstep, y_step=ystep, x_offset=xoffset, y_offset=yoffset, gonio_xy=(gx, gy), grid_index=grid_index, parent=self,) + grid = CstROI.Grid( x_step=xstep, y_step=ystep, x_offset=xoffset, y_offset=yoffset, gonio_xy=(gx, gy), grid_index=grid_index, parent=self,) self._grids.append(grid) grid.calculate_gonio_xy() grid.sigRemoveRequested.connect(lambda g=grid: self.remove_grid(g)) @@ -2754,7 +2801,7 @@ class Main(QMainWindow, Ui_MainWindow): if not (add_xtals and draw_xtals): continue _log.debug(f"adding {'fiducial' if is_fiducial else 'xtal'} mark #{n}: {is_fiducial} {gx:.3f}, {gy:.3f}, {cx:.1f}, {cy:.1f}") - m = CrystalCircle( + m = CstROI.CrystalCircle( gonio_xy=(gx, gy), parent=self, model_row_index=n, @@ -3277,6 +3324,9 @@ def main(): else: app._camera = camera.epics_cam() #app._camera.run() is called in center_piece_update + app._geometry=geometry.geometry() + + app_icon = QtGui.QIcon() app_icon.addFile("artwork/logo/16x16.png", QtCore.QSize(16, 16))