From 7deda365c1c327627825468c6b8d2575077d28e2 Mon Sep 17 00:00:00 2001 From: Thierry Zamofing Date: Tue, 30 Aug 2022 15:46:45 +0200 Subject: [PATCH] saving document, plane and fiducal fitter --- app_config.py | 37 ++++++++++++++++++++++--- camera.py | 10 +++++-- geometry.py | 76 +++++++++++++++++++++++++++++++++++++-------------- pyqtUsrObj.py | 21 +++++++++++++- swissmx.py | 68 ++++++++++++++++++++++++++++++++++++--------- 5 files changed, 172 insertions(+), 40 deletions(-) diff --git a/app_config.py b/app_config.py index f053a1d..88d93f4 100644 --- a/app_config.py +++ b/app_config.py @@ -10,9 +10,25 @@ _log = logging.getLogger(__name__) from PyQt5.QtCore import QSettings from PyQt5.QtWidgets import QApplication, QLineEdit -import os, json, yaml +import json +import numpy as np import GenericDialog + +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): + return obj.tolist() + elif type(obj) not in (dict,list,str,int): + _log.error('dont know how to json') + return repr(obj) + return json.JSONEncoder.default(self, obj) + class AppCfg(QSettings): GEO_OPT_CTR='geometry/opt_ctr' @@ -87,8 +103,7 @@ class AppCfg(QSettings): self.setValue(AppCfg.GEO_BEAM_POS, [23.4, -54.2]) # beam position relativ to optical center in mm if AppCfg.GEO_PIX2POS not in keys: - _log.warning(f'{AppCfg.GEO_OPT_CTR} not defined. use default') - import numpy as np + _log.warning(f'{AppCfg.GEO_PIX2POS} not defined. use default') lut_pix2pos=(np.array([1., 200., 400., 600., 800., 1000.]), np.array([[[ 2.42827273e-03, -9.22117396e-05], [-1.10489804e-04, -2.42592492e-03]], @@ -106,7 +121,6 @@ class AppCfg(QSettings): if AppCfg.GEO_OPT_CTR not in keys: _log.warning(f'{AppCfg.GEO_OPT_CTR} not defined. use default') - import numpy as np opt_ctr=np.array([603.28688025, 520.01112846]) self.setValue(AppCfg.GEO_OPT_CTR, opt_ctr) @@ -129,10 +143,25 @@ class AppCfg(QSettings): # print(data) def setValue(self, key: str, val): #overload to debug + # only simple lists, str, int, float can not be serialized nicely + t=type(val) + if key in (AppCfg.GEO_PIX2POS): + val=json.dumps(val, cls=MyJsonEncoder) + elif key in (AppCfg.GEO_OPT_CTR,AppCfg.GEO_BEAM_SZ,AppCfg.GEO_BEAM_POS,): + val=val.tolist() + elif type(val)==tuple: + val=list(val) return super(AppCfg, self).setValue(key,val) def value(self,key,*vargs,**kwargs): #overload to debug val=super(AppCfg, self).value(key,*vargs,**kwargs) + if key in (AppCfg.GEO_PIX2POS): + val=json.loads(val)#, object_hook=MyJsonDecoder) + val=(np.array(val[0]),np.array(val[1])) + elif key in (AppCfg.GEO_BEAM_SZ,AppCfg.GEO_BEAM_POS,): + val=np.array(tuple(map(float, val)))/1000 + elif key in (AppCfg.GEO_OPT_CTR): + val=np.array(tuple(map(float, val))) return val #@property #def value(self): diff --git a/camera.py b/camera.py index f7385ff..57d203c 100755 --- a/camera.py +++ b/camera.py @@ -258,9 +258,15 @@ class epics_cam(object): imgSeq=np.ndarray(shape=(len(glb),sz[1],sz[0]),dtype=np.uint8) # shape is (n,h,w) for i,fn in enumerate(glb): img=PIL.Image.open(fn) - assert(img.mode=='L') # 8 bit grayscale + #assert(img.mode=='L') # 8 bit grayscale assert(sz==img.size) - imgSeq[i,:,:]=np.array(img.getdata()).reshape(sz[::-1]) + if img.mode in ('L'): + #imgSeq[i, :, :]=np.asarray(img) + imgSeq[i,:,:]=np.array(img.getdata()).reshape(sz[::-1]) + elif img.mode=='LA': + imgSeq[i, :, :]=np.asarray(img)[:,:,0] + else: + raise TypeError(f'unsupported image mode format {img.mode}') pic=imgSeq[i] epics_cam.set_fiducial(pic, 255) diff --git a/geometry.py b/geometry.py index 3720043..b507da3 100755 --- a/geometry.py +++ b/geometry.py @@ -268,7 +268,7 @@ class geometry: pass @staticmethod - def least_square_trf(points): + def least_square_trf(points,fid=None,sort=True): # inputs are 4 points of a parallelogram # this function returns the least square transformation #[ px] [a b c ] [ q ] @@ -287,35 +287,71 @@ class geometry: # # A *aa = y - A=np.array(( - (0,0,1,0,0,0), - (0,0,0,0,0,1), - (0,1,1,0,0,0), - (0,0,0,0,1,1), - (1,0,1,0,0,0), - (0,0,0,1,0,1), - (1,1,1,0,0,0), - (0,0,0,1,1,1)), np.float) + # A=np.array(( + # (0,0,1,0,0,0), + # (0,0,0,0,0,1), + # (0,1,1,0,0,0), + # (0,0,0,0,1,1), + # (1,0,1,0,0,0), + # (0,0,0,1,0,1), + # (1,1,1,0,0,0), + # (0,0,0,1,1,1)), np.float) + if fid is None: + fid=np.array(((0,0),(0,1),(1,0),(1,1))) + elif type(fid)!=np.ndarray: + fid=np.array(fid) y=points # sort points p00 p01 p10 p11 - s=y[:,1].argsort() - y=y[s,:] - s=y[:2,0].argsort() - y[:2,:]=y[:2,:][s,:] - s=y[2:,0].argsort() - y[2:,:]=y[2:,:][s,:] + if sort: + # p01 ----------- p11 + # | | + # | | + # p00-------------p10 + def sort(a): + s=a[:,0].argsort() + a=a[s,:] + s=a[:2,1].argsort() + a[:2,:]=a[:2,:][s,:] + s=a[2:,1].argsort() + a[2:,:]=a[2:,:][s,:] + return a + y=sort(y) + fid=sort(fid) + + A=np.ndarray((len(fid)*2,6)) + i=0 + for q,r in fid: + A[i] =(q,r,1,0,0,0) + A[i+1]=(0,0,0,q,r,1) + i+=2 + y=np.asmatrix(y.ravel()).T A=np.asmatrix(A) aa=(A.T*A).I*A.T*y aa=aa.reshape((2,3)) return aa - @staticmethod - def least_square_plane(points): + def least_square_plane(self,points): #inputs are multiple (x,y,z) points # this function returns the parameters of least square fitted plane x=x,y=y,z=ax+bx+c - pass + + # a b c + # [px1 py1 1 ] [a] [pz1] + # [px2 py2 1 ] [b] [pz2] + # [... ... 1 ]*[c]=[...] + # [pxn pyn 1 ] [pzn] + A=np.ndarray((points.shape[0],3)) + y=points[:,2] + A[:,2]=1 + A[:,0:2]=points[:,:2] + y=np.asmatrix(y.ravel()).T + A=np.asmatrix(A) + aa=(A.T*A).I*A.T*y + aa=aa.A.ravel() + for p in points: + print(f'{p}->{aa[0]*p[0]+aa[1]*p[1]+aa[2]}') + self._fitPlane=aa if __name__=="__main__": @@ -473,7 +509,7 @@ if __name__=="__main__": [40, 35], [10, 35], [40, 15]]) - pts=-pts + #pts=-pts for p in pts: diff --git a/pyqtUsrObj.py b/pyqtUsrObj.py index 99917da..a51ad9c 100644 --- a/pyqtUsrObj.py +++ b/pyqtUsrObj.py @@ -12,6 +12,8 @@ TODO: Dimension of pyqtgraph is in um not pixel (as now) ''' #import initExample ## Add path to library (just for examples; you do not need this) +import logging +_log=logging.getLogger(__name__) import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui @@ -316,7 +318,8 @@ class Path(pg.ROI): p.drawLines(lh,lv) def __repr__(self): - s=f'{self.__class__.__name__}:(pos:{itr2str(self.pos())}, size:{itr2str(self.size())}, cnt:{self._cnt}, ficucialScale:{self._fidScl}}}' + s=f'{self.__class__.__name__}:(pos:{itr2str(self.pos())}, size:{itr2str(self.size())}, numFid:{self._fiducial.shape[0]}, numPts:{self._path.shape[0]}, ficucialScale:{self._fidScl}}}' + return s def obj2json(self,encoder): @@ -373,7 +376,15 @@ class FixTargetFrame(pg.ROI): } def __init__( self, pos=(0,0), size=(100,100), tpl='test', dscr=None, **kwargs): + trf=kwargs.pop('trf',None) pg.ROI.__init__(self, pos, size, **kwargs) + if trf is not None: + t=self.transform() + t.setMatrix(trf[0][0], trf[0][1], 0, + trf[1][0], trf[1][1], 0, + 0, 0, 1) + self.setTransform(t) + #fiducial type 0: 5 squares with pitch 120 um if dscr is not None: self._dscr=dscr @@ -383,6 +394,9 @@ class FixTargetFrame(pg.ROI): self.addScaleHandle([1, 1], [0, 0]) self.addScaleHandle([0, 0], [1, 1]) self.addScaleRotateHandle([1, 0], [0, 0]) + #self.sigHoverEvent.connect(self.hover) +# def hover(self): +# _log.debug(f'hover {self}') def paint(self, p, *args): #pg.ROI.paint(self, p, *args) @@ -445,6 +459,11 @@ class FixTargetFrame(pg.ROI): 'size':tuple(self.size()), 'dscr': self._dscr } + trf=self.transform() + if not trf.isIdentity(): + obj_info(trf) + trf=((trf.m11(),trf.m12()),(trf.m21(),trf.m22())) + jsn['trf']=trf return jsn diff --git a/swissmx.py b/swissmx.py index a907f8c..2cd6473 100755 --- a/swissmx.py +++ b/swissmx.py @@ -64,6 +64,7 @@ TASK_SETUP_CAMERA = "setup_camera" TASK_SETUP_ROI = "setup_rois" TASK_SAMPLE_SELECTION = "task_sample_selection" TASK_SCREENING = "screening" +TASK_FIX_TARGET = "fix_trg" TASK_GRID = "grid" TASK_PRELOCATED = "prelocated" TASK_HELICAL = "helical" @@ -407,13 +408,14 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): self._goGrid=grid=pg.GridItem() # green grid and labels grid.opts['pen']=QPen(QColor(0, 255, 0)) grid.opts['textPen']=QPen(QColor(0, 255, 0)) - #tr.reset() + tr.reset() #grid.setTransform(tr) # assign transform vb.addItem(grid) #--- beam marker --- bm_sz=np.array((50, 40)) # it is immidiatly repositioned in zoom_changed_cb self._goBeamMarker=bm=UsrGO.Marker(-opt_ctr-bm_sz/2,bm_sz,mode=0) + bm.setTransform(tr) # assign transform vb.addItem(bm) #--- opctical center ---- @@ -717,8 +719,8 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): _log.warning(e) else: opt_ctr=geo._opt_ctr - bm_sz=np.array(tuple(map(float, cfg.value(AppCfg.GEO_BEAM_SZ))))/1000 - bm_pos=np.array(tuple(map(float, cfg.value(AppCfg.GEO_BEAM_POS))))/1000 + bm_sz=cfg.value(AppCfg.GEO_BEAM_SZ) + bm_pos=cfg.value(AppCfg.GEO_BEAM_POS) bm_sz=np.abs(geo.pos2pix(bm_sz)) bm_pos=-opt_ctr-geo.pos2pix(bm_pos)-bm_sz/2 bm=self._goBeamMarker @@ -936,7 +938,7 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): #_log.debug(f"vb pos {self.vb.mapFromScene(p)}") #pixel position on the scene (including black frame) #for o in self.vb.childGroup.childItems(): # _log.debug(f"{type(o)} pos {o.mapFromScene(p)}") #pixel position on the scene (including black frame) - + _log.debug(f"currentItem:{event.currentItem}") task = self.active_task() if task==TASK_SETUP_GEOMETRY_CALIB: self.mouse_click_event_geometry_calib(event) @@ -1235,11 +1237,40 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): def execute_collection(self): app=QApplication.instance() geo=app._geometry + #zoom=app._zoom.get_val() task = self.active_task() - self._is_aborted = False - method = self._tabs_daq_methods.currentWidget().accessibleName() + #self._is_aborted = False + #method = self._tabs_daq_methods.currentWidget().accessibleName() - if task == TASK_GRID: + + + if task == TASK_FIX_TARGET: + #self.re_connect_collect_button(callback=self.collect_abort_grid, accessibleName="grid_abort", label="Abort Grid Scan",) + #self._inspect = self._grid_inspect_area + #self._inspect.setPlainText("") + + fast_x=self.tweakers["fast_x"]; + fast_y=self.tweakers["fast_y"] + fx=fast_x.get_val() + fy=fast_y.get_val() + opt_ctr=geo._opt_ctr + for go in self._goTracked['objLst']: + points=go.get_points() #points in coordinate system of ROI + # names consists of abrevations + # part 0: po=position sz=size dt=delta + # part 1: px=pixel eu=engineering units (e.g. mm) + po_px=go.pos() + sz_px=go.size() + tr=go.transform() # TODO: this is not yet used + UsrGO.obj_info(tr) + dt_px=-opt_ctr-po_px + dt_eu=geo.pix2pos(dt_px)+(fx,fy) + for i in range(points.shape[0]): + points[i,:]=dt_eu+geo.pix2pos(points[i,:])#*tr + + self.daq_collect_points(points, visualizer_method="grid", visualizer_params=None) + + elif task == TASK_GRID: self.re_connect_collect_button( callback=self.collect_abort_grid, accessibleName="grid_abort", @@ -1541,6 +1572,7 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): self._moduleFixTarget =mft = ModuleFixTarget.WndFixTarget(self,data) tab = self._tabs_daq_methods.insertTab(0,mft,'Fix Target') + mft.setAccessibleName(TASK_FIX_TARGET) self._tabs_daq_methods.setCurrentWidget(mft) #set this as the active tabs mft._cbType.addItems(["Fiducial", "FixTarget(12.5x12.5)", "FixTarget(23.0x23.0)", "FixTarget()", "Grid()"]) @@ -1677,20 +1709,30 @@ class SwissMxWnd(QMainWindow, Ui_MainWindow): for go in reversed(objLst): if type(go)==UsrGO.FixTargetFrame: if j==4: - trf=geometry.geometry.least_square_trf(ptFitTrf) + #fid=np.array(((0.1, 0.1), (0.1, 1.1), (1.1, 0.1), (1.1, 1.1))) + fid=np.array(go._dscr['fiducial']['pos']) + sz=np.array(go._dscr['size']) + fid=fid/sz + trf=geometry.geometry.least_square_trf(ptFitTrf,fid) tr=go.transform() - tr.setMatrix(100, 0, 0, - 0, 100, 0, - 0, 0, 1) + tr.setMatrix(1, trf[1,0]/trf[0,0], 0, + trf[0,1]/trf[1,1], 1, 0, + 0, 0, 1) go.setTransform(tr) go.setPos(trf[:,2]) - go.setSize((1,1)) + #go.setSize((1,1)) + sz=(trf[0,0],trf[1,1]) + go.setSize(sz) + j=0 elif type(go)==UsrGO.Fiducial: ptFitTrf[j]=go.pos()+go.size()/2 ptFitPlane[i]=go._xyz i+=1;j+=1 - plane=geometry.geometry.least_square_plane(ptFitPlane) + app=QApplication.instance() + geo=app._geometry + if n>=3: + geo.least_square_plane(ptFitPlane) # **************** OBSOLETE AND/OR OLD STUFF ****************