class QuadDetection(object): ''' detect the corners of a (bright) quadrilateral object in an image e.g. a PV cell/module in an EL image ''' def __init__(self, img=None, vertices=None, refinePositions=True): ''' @param img -> input image @paramn vertices -> routh estimate of corner positions @refinePositions -> whether to refine (found) corner positions ''' self.img = imread(img, 'gray') self.vertices = vertices self._pc = None if self.vertices is None: lines = self._findQuadLines() if refinePositions: lines = self._refineLines(lines) self.vertices = self._verticesFromLines(lines) def correct(self, img=None,**kwargs): ''' correct perspective forwards kwargs to PerspectiveCorrection ''' if img is None: img = self.img if self._pc: img = self._pc.correct(img) img = img[self._shape] return img else: self._pc = PerspectiveCorrection(img.shape, **kwargs) self._pc.setReference(self.vertices) img = self._pc.correct(img) self._corrected = True return img @staticmethod def _to8bitImg(img): if img.dtype == np.uint8: return img r = signalRange(img) return toUIntArray(img, dtype=np.uint8, range=r) @staticmethod def _findEdgeLine(img, axis=0, start=0, stop=-1, direction=1): #find the approximate edge line of and object within an image #along a given axis s = img.shape if start != 0 or stop != -1: #cut image if direction == 1: cut = slice(start,stop) else: cut = slice(stop,start,-1) if axis==0: img = img[cut,:] else: img = img[:,cut] sx = s[int(~axis)] x = np.arange(sx) #return first non=zero value along given axis y = np.argmax(img, axis=axis) valid = y!=0 if valid.sum() > 0.2*sx: y = y[valid] x = x[valid] #filter outliers: p = polyFitIgnoringOutliers(x,y,deg=1, niter=5, nstd=1) #extract edge points: x0,x1 = x[0],x[-1] y0,y1 = p(x0), p(x1) if axis == 1: x0,x1,y0,y1 = y0,y1,x0,x1 #move points to actual position in image if direction == 1: if start != 0: if axis == 0: y0, y1 = y0+start, y1+start else: x0, x1 = x0+start, x1+start else: if stop == -1: stop = s[axis] if axis == 0: y0, y1 = stop-y0, stop-y1 else: x0, x1 = stop-x0, stop-x1 return (x0,y0,x1,y1) def _findQuadLines(self): img = self.img #TODO: give multiple options to find line #take first ...whatever #_, thresh = cv2.threshold(self._to8bitImg(self.img), 0, 255, cv2.cv.CV_THRESH_OTSU) thresh = img > signalMinimum(img) #remove small features: thresh = minimum_filter(thresh,5) thresh = maximum_filter(thresh,5) s0,s1 = img.shape #edge lines: ltop = self._findEdgeLine( thresh, axis=0, stop=s0/2 ) lbottom = self._findEdgeLine(thresh, axis=0, start=s0/2, direction=-1) lleft = self._findEdgeLine( thresh, axis=1, stop=s1/2 ) lright = self._findEdgeLine( thresh, axis=1, start=s1/2, direction=-1) return ltop, lbottom, lleft, lright @staticmethod def _verticesFromLines((ltop, lbottom, lleft, lright)): #grid vertices vie lines intersection: return np.array((ln.intersection(ltop, lleft), ln.intersection(ltop, lright), ln.intersection(lbottom, lright), ln.intersection(lbottom, lleft) ) ) @staticmethod def _linesFromvertices(vertices): c0,c1,c2,c3 = vertices ltop = c3[0],c3[1],c2[0],c2[1] lbottom = c0[0],c0[1],c1[0],c1[1] lleft = c0[0],c0[1],c3[0],c3[1] lright = c1[0],c1[1],c2[0],c2[1] return ltop, lbottom, lleft, lright def _refineLines(self, lines):#, plot=False, sub_height=20): ''' fit border lines through fitting to highest gradient ''' lines = list(lines) #sign is negative, when going from low to high intensity signs = (1,-1,1,-1) sub_height = min(91,max(7,self.img.shape[0]/200)) for m, (l,sign) in enumerate(zip(lines, signs)): sub = alignImageAlongLine(self.img, l, sub_height) dsub = sign*cv2.Sobel(sub,cv2.CV_64F,0,1,ksize=5) d0, d1 = minimumLineInArray(dsub, relative=True) new_l = ln.translate2P(l,d0, d1) lines[m]=new_l return lines def drawVertices(self, img=None, color=None, thickness=4): if img is None: img = self.img if color is None: color = img.max()-1 for l in self._linesFromvertices(self.vertices.astype(int)): cv2.line(img, tuple(l[:2]), tuple(l[2:]), color, thickness=thickness) return img
class PerspectiveCorrection(Tool): ''' Correct the rotation and position of a quad distorted through perspective. The edge points of the quad can be chosen manually or automated ''' icon = 'quadDetection.svg' def __init__(self, imageDisplay): Tool.__init__(self, imageDisplay) _import() self.quadROI = None self.outDisplay = None self._refPn = [] pa = self.setParameterMenu() self.calFileTool = self.showGlobalTool(CalibrationFile) self.pExecOn = pa.addChild({ 'name': 'Execute on', 'type': 'list', 'value': 'all images', 'limits': ['current image', 'all images', 'last image'] }) self.createResultInDisplayParam(pa) self.pRef = pa.addChild({ 'name': 'Reference', 'type': 'list', 'value': 'Object profile', 'limits': ['Object profile', 'Reference image', 'Reference points'] }) # HOMOGRAPHY THROUGH QUAD self.pBorder = self.pRef.addChild({ 'name': 'Border', 'type': 'int', 'value': 50, 'min': 0 }) # pro = GridDetection is not None self.pManual = self.pRef.addChild({ 'name': 'Manual object detection', 'type': 'bool', 'value': True, 'readonly': True }) # self.pCells = self.pRef.addChild({ # 'name': 'Rectify cells', # 'type': 'bool', # 'value': False, # 'readonly': not pro}) # self.pCellsX = self.pCells.addChild({ # 'name': 'X', # 'type': 'int', # 'value': 0, # 'limits': (0, 100)}) # self.pCellsY = self.pCells.addChild({ # 'name': 'Y', # 'type': 'int', # 'value': 0, # 'limits': (0, 100)}) # def fn(_p, _v): # return self._updateROI() # self.pCellsX.sigValueChanged.connect(fn) # self.pCellsY.sigValueChanged.connect(fn) # self.pMask = self.pRef.addChild({ # 'name': 'Create Mask', # 'type': 'bool', # 'value': True}) # self.pMaskOffset = self.pMask.addChild({ # 'name': 'Offset', # 'type': 'int', # 'value': 0, # 'min': 0, # 'suffix': 'px'}) # self.pMaskDetect = self.pMask.addChild({ # 'name': 'Detect parameters', # 'type': 'bool', # 'value': False}) # self.pSubcells = self.pMaskDetect.addChild({ # 'name': 'Number busbars', # 'type': 'int', # 'value': 3, # 'limits': (0, 109)}) # self.pCellOrient = self.pMaskDetect.addChild({ # 'name': 'Busbar Orientation', # 'type': 'list', # 'value': 'horiz', # 'values': ['horiz', 'vert']}) # self.pCellShape = self.pMaskDetect.addChild({ # 'name': 'Cell shape', # 'type': 'list', # 'value': 'square', # 'values': ['square', 'pseudo square']}) # self.pMask.sigValueChanged.connect(lambda param, val: # [ch.show(val) for ch in param.childs]) # self.pMask.setValue(False) # self.pMaskDetect.sigValueChanged.connect(lambda param, val: # [ch.show(not val) for ch in param.childs]) # self.pMaskDetect.setValue(True) # HOMOGRAPHY THROUGH REF IMAGE self.pRefImgChoose = self.pRef.addChild({ 'name': 'Reference image', 'value': 'From display', 'type': 'menu', 'visible': False }) self.pRefImg = self.pRefImgChoose.addChild({ 'name': 'Chosen', 'value': '-', 'type': 'str', 'readonly': True }) self.pRefImgChoose.aboutToShow.connect( lambda menu: self.buildOtherDisplayLayersMenu( menu, self._setRefImg, includeThisDisplay=True)) # self.pSubPx = self.pRef.addChild({ # 'name': 'Sub-pixel alignment', # 'value': True, # 'type': 'bool', # 'visible': False, # 'enabled': pro}) # self.pSubPx_x = self.pSubPx.addChild({ # 'name': 'cells x', # 'value': 12, # 'type': 'int', # 'limits': (1, 100)}) # self.pSubPx_y = self.pSubPx.addChild({ # 'name': 'cells y', # 'value': 9, # 'type': 'int', # 'limits': (1, 100)}) # self.pSubPx_neigh = self.pSubPx.addChild({ # 'name': 'n neighbors', # 'value': 2, # 'type': 'int', # 'limits': (1, 100)}) # self.pSubPx_maxDev = self.pSubPx.addChild({ # 'name': 'max dev', # 'value': 30, # 'type': 'int', # 'min': 1}) # # self.pSubPx.sigValueChanged.connect(lambda param, val: # [ch.show(val) for ch in param.childs]) # self.pSubPx.setValue(False) self.pRef.sigValueChanged.connect(self._pRefChanged) self.pManual.sigValueChanged.connect(self._setupQuadManually) pShowPlane = self.pManual.addChild({ 'name': 'Show Plane', 'type': 'bool', 'value': True }) pShowPlane.sigValueChanged.connect(self._showROI) self.pCalcOutSize = pa.addChild({ 'name': 'Output size', 'type': 'list', 'value': 'Calculate', 'limits': ['Calculate', 'Current Size', 'Manual'] }) self.pOutWidth = self.pCalcOutSize.addChild({ 'name': 'Image width [px]', 'type': 'int', 'value': 1000, 'visible': False }) self.pOutHeight = self.pCalcOutSize.addChild({ 'name': 'Image height [px]', 'type': 'int', 'value': 600, 'visible': False }) self.pCalcOutSize.sigValueChanged.connect(self._pCalcOutSizeChanged) # self.pCalcAR = pa.addChild({ # 'name': 'Calculate aspect ratio', # 'type': 'bool', # 'value': True, # 'tip': ''}) # # self.pObjWidth = self.pCalcAR.addChild({ # 'name': 'Object width [mm]', # 'type': 'float', # 'value': 0, # 'visible': False}) # # self.pObjHeight = self.pCalcAR.addChild({ # 'name': 'Object height [mm]', # 'type': 'float', # 'value': 0, # 'visible': False # }) # self.pCalcAR.sigValueChanged.connect(lambda param, val: # [ch.show(not val) for ch in param.childs]) self.pCorrViewFactor = pa.addChild({ 'name': 'Correct Intensity', 'type': 'bool', 'value': False, 'tip': 'For realistic results choose a camera calibration' }) # self.pDrawViewFactor = self.pCorrViewFactor.addChild({ # 'name': 'Show tilt factor', # 'type': 'bool', # 'value': False}) # self.pCorrViewFactor.sigValueChanged.connect(lambda param, val: # self.pDrawViewFactor.show(val)) self.pLive = pa.addChild({ 'name': 'Live', 'tip': 'Update result whenever one of the parameter was changed', 'type': 'bool', 'value': False }) self.pOverrideOutput = pa.addChild({ 'name': 'Override output', 'type': 'bool', 'value': True }) def _pCalcOutSizeChanged(self, _p, v): # self.pCalcAR.show(v == 'Calculate') self.pOutWidth.show(v == 'Manual') self.pOutHeight.show(v == 'Manual') def _setRefImg(self, display, layernumber, layername): ''' extract the reference image and -name from a given display and layer number ''' im = display.widget.image self._refImg_from_own_display = -1 self._refImg = im[layernumber] if display == self.display: self._refImg_from_own_display = layernumber self.pRefImg.setValue(layername) def _showROI(self, _p, value): if self.quadROI is not None: self.quadROI.show() if value else self.quadROI.hide() def _setupQuadManually(self, _p, value): if value: if not self.quadROI: self.quadROI = self._createROI() else: self.quadROI.show() elif self.quadROI: self.quadROI.hide() def _createRefPn(self): w = self.display.widget r = w.view.vb.viewRange() p = ((r[0][0] + r[0][1]) / 2, (r[1][0] + r[1][1]) / 2) s = [(r[0][1] - r[0][0]) * 0.1, (r[1][1] - r[1][0]) * 0.1] pos = np.array([[p[0] - s[0], p[1] + s[1]], [p[0] - s[0], p[1] - s[1]], [p[0] + s[0], p[1] - s[1]]]) rFrom = pg.PolyLineROI(pos, pen=(255, 0, 0), movable=False) rTo = pg.PolyLineROI(pos + (5, 5), pen=(0, 255, 0), movable=False) w.view.vb.addItem(rFrom) w.view.vb.addItem(rTo) return (rFrom, rTo) def _pRefChanged(self, _p, val): x = val == 'Reference image' self.pRefImgChoose.show(x) # self.pSubPx.show(x) x = val == 'Object profile' self.pManual.show(x) self.pCells.show(x) self.pBorder.show(x) self.pMask.show(x) vv = val == 'Reference points' if vv: if not len(self._refPn): self._refPn = self._createRefPn() [r.show() for r in self._refPn] else: [r.hide() for r in self._refPn] self.pCorrViewFactor.show(not vv) # def _updateROI(self, roi=None): # if roi is None: # roi = self.quadROI # if roi is not None: # roi.p.param('X').setValue(self.pCellsX.value()) # roi.p.param('Y').setValue(self.pCellsY.value()) def _createROI(self, display=None): if display is None: display = self.display t = display.tools['Selection'] # check whether already a grid ROI exists: quadROI = t.findPath(PerspectiveGridROI) if quadROI is None: # no grid ROI there -> create one! t.pType.setValue('PerspectiveGrid') quadROI = t.new() # pNew.sigActivated.emit(t.pNew) quadROI.sigRegionChanged.connect(lambda: self.pManual.setValue(True)) # self._updateROI(quadROI) return quadROI def activate(self): if self.pLive.value(): self._prepare() self.display.widget.item.sigImageChanged.connect( self._processAndDone) else: self.startThread(self._prepareAndProcess, self._done) def _prepare(self): w = self.display.widget img = w.image[w.currentIndex] v = self.pRef.value() if v == 'Reference points': self.pc = None return # do in self.process try: c = self.calFileTool.currentCameraMatrix() except AttributeError: c = None # height/width: # if self.pCalcAR.value(): # w, h = None, None # else: # if self.pCalcOutSize.value() == 'Current Size': # w, h = img.shape[:2] # else: # w = self.pObjWidth.value() # if w == 0: # w = None # h = self.pObjHeight.value() # if h == 0: # h = None # output size: size = (None, None) if not self.pCalcOutSize.value(): size = (self.pOutWidth.value(), self.pOutHeight.value()) self._pc_args = dict( # obj_width_mm=h, # obj_height_mm=w, cameraMatrix=c, do_correctIntensity=self.pCorrViewFactor.value(), new_size=size) # HOMOGRAPHY THROUGH REFERENCE IMAGE self.pc = PC(img.shape, border=self.pBorder.value(), **self._pc_args) if v == 'Reference image': # INIT: self.pc.setReference(self._refImg) else: # HOMOGRAPHY THROUGH QUAD # vertices = None # if self.pManual.value(): if not self.quadROI: self.quadROI = self._createROI() raise Exception('need to fit quad first.') vertices = self.quadROI.vertices() # else: # vertices = QuadDetection(img).vertices # print(vertices, 8888888888888888) # if GridDetection is None: # self.pc = PC(img.shape, **self._pc_args) self.pc.setReference(vertices) # else: # if self.pMaskDetect.value(): # nSublines = None # cellshape = None # else: # ns = self.pSubcells.value() # if self.pCellOrient.value() == 'horiz': # nSublines = ([ns], [0]) # else: # nSublines = ([0], [ns]) # cellshape = self.pCellShape.value() # # gy = self.pCellsY.value() # gx = self.pCellsX.value() # if 0 in (gx, gy): # grid = None # else: # grid = gy, gx # self.pc = GridDetection(img=img, # border=self.pBorder.value(), # vertices=vertices, # shape=cellshape, # grid=grid, # nSublines=nSublines, # refine_cells=self.pCells.value(), # refine_sublines=self.pMask.value()) def _process(self): w = self.display.widget img = w.image out = [] v = self.pRef.value() if v == 'Reference points': # 2x3 point warp: r0, r1 = self._refPn pts0 = np.array([(h['pos'].y(), h['pos'].x()) for h in r0.handles]) + r0.pos() pts1 = np.array([(h['pos'].y(), h['pos'].x()) for h in r1.handles]) + r1.pos() # TODO: embed in PyerspectiveCorrection M = cv2.getAffineTransform(pts0.astype(np.float32), pts1.astype(np.float32)) for n, i in enumerate(img): out.append( # TODO: allow different image shapes cv2.warpAffine(i, M, w.image.shape[1:3], borderValue=0)) else: r = v == 'Reference image' e = self.pExecOn.value() for n, i in enumerate(img): if (e == 'all images' or (e == 'current image' and n == w.currentIndex) or (e == 'last image' and n == len(img) - 1)): if not (r and n == self._refImg_from_own_display): corr = self.pc.correct(i) # if r and self.pSubPx.value(): # corr = subPixelAlignment( # corr, self._refImg, # niter=20, # grid=(self.pSubPx_y.value(), # self.pSubPx_x.value()), # method='smooth', # # maxGrad=2, # concentrateNNeighbours=self.pSubPx_neigh.value(), # maxDev=self.pSubPx_maxDev.value())[0] out.append(corr) return out def _done(self, out): change = 'PerspectiveFit' self.outDisplay = self.handleOutput(out, title=change) # if not self.outDisplay or self.outDisplay.isClosed(): # self.outDisplay = self.display.workspace.addDisplay( # origin=self.display, # changes=change, # data=out, # title=change) # else: # self.outDisplay.widget.update(out) # self.outDisplay.widget.updateView() if self.pRef.value() == 'Object profile': # show grid in display: if not self.pManual.value(): if self.quadROI is None: self.quadROI = self._createROI() # TODO: unclean having perspectiveCorrection in GridDetection pc = self.pc # if isinstance(pc, GridDetection): # # if GridDetection is not None: # gx, gy = pc.opts['grid'] # self.pCellsX.setValue(gx) # self.pCellsY.setValue(gy) # pc = self.pc._pc self.quadROI.setVertices(pc.quad) print(pc.quad) self.quadROI.show() # inherit corrected grid to output display: # TODO: unclean self.outDisplay.clicked.emit(self.outDisplay) # init tools quadROI = self._createROI(self.outDisplay) b = self.pBorder.value() sy, sx = out[0].shape[:2] v = np.array([(b, sy - b), (sx - b, sy - b), (sx - b, b), (b, b)]) quadROI.setVertices(v) # synchronize handle movemend in both this and output display: # quadROI.sigRegionChanged.connect(self.___a) # if self.pCorrViewFactor.value() and self.pDrawViewFactor.value(): # if not self.outDisplayViewFactor: # TODO: mask generation in extra tool # if self.pMask.value() and self.pMask.isVisible(): # self.outDisplay.widget.addColorLayer( # layer=self.pc.mask(offs=self.pMaskOffset.value()), # name='Grid mask') if not self.pOverrideOutput.value(): self.outDisplay = None if not self.pLive.value(): self.setChecked(False) del self.pc def _prepareAndProcess(self): self._prepare() return self._process() def _processAndDone(self): out = self._process() self._done(out) def deactivate(self): try: w = self.display.widget w.item.sigImageChanged.disconnect(self._processAndDone) except: pass
class PerspectiveCorrection(Tool): ''' Correct the rotation and position of a quad distorted through perspective. The edge points of the quad can be chosen manually or automated ''' icon = 'quadDetection.svg' def __init__(self, imageDisplay): Tool.__init__(self, imageDisplay) self.quadROI = None self.outDisplay = None self.outDisplayViewFactor = None self._cLayerLines = None pa = self.setParameterMenu() self.calFileTool = self.showGlobalTool(CalibrationFile) self.pExecOn = pa.addChild({ 'name':'Execute on', 'type':'list', 'value':'all images', 'limits':['current image', 'all images', 'last image']}) self.pRef = pa.addChild({ 'name':'Reference', 'type':'list', 'value':'Object profile', 'limits':['Object profile','Reference image']}) #HOMOGRAPHY THROUGH QUAD self.pManual = self.pRef.addChild({ 'name':'Manual object detection', 'type':'bool', 'value':False}) # self.pSnap = self.pManual.addChild({ # 'name':'Snap at edges', # 'type':'bool', # 'value':False, # 'visible':False}) #HOMOGRAPHY THROUGH REF IMAGE self.pRefImgChoose = self.pRef.addChild({ 'name':'Reference image', 'value':'From display', 'type':'menu', 'visible':False}) self.pRefImg = self.pRefImgChoose.addChild({ 'name':'Chosen', 'value':'-', 'type':'str', 'readonly':True}) self.pRefImgChoose.aboutToShow.connect(lambda menu: self.buildOtherDisplayLayersMenu(menu, self._setRefImg) ) self.pRef.sigValueChanged.connect(self._pRefChanged) # self.pSnap.sigValueChanged.connect(self._pSnapChanged) self.pManual.sigValueChanged.connect(self._setupQuadManually) pShowPlane = self.pManual.addChild({ 'name':'Show Plane', 'type':'bool', 'value':True}) pShowPlane.sigValueChanged.connect(self._showROI) self.pCalcOutSize = pa.addChild({ 'name':'Calculate output size', 'type':'bool', 'value':True, 'tip':''}) self.pOutWidth = self.pCalcOutSize.addChild({ 'name':'Image width [px]', 'type':'int', 'value':1000, 'visible':False}) self.pOutHeight = self.pCalcOutSize.addChild({ 'name':'Image height [px]', 'type':'int', 'value':600, 'visible':False }) self.pCalcOutSize.sigValueChanged.connect( lambda p,v:[self.pCalcAR.show(v), self.pOutWidth.show(not v), self.pOutHeight.show(not v) ]) self.pCalcAR = pa.addChild({ 'name':'Calculate aspect ratio', 'type':'bool', 'value':True, 'tip':''}) self.pObjWidth = self.pCalcAR.addChild({ 'name':'Object width [mm]', 'type':'float', 'value':0, 'visible':False}) self.pObjHeight = self.pCalcAR.addChild({ 'name':'Object height [mm]', 'type':'float', 'value':0, 'visible':False }) self.pCalcAR.sigValueChanged.connect(lambda param, val: [ch.show(not val) for ch in param.childs]) self.pCorrViewFactor = pa.addChild({ 'name':'Correct Intensity', 'type':'bool', 'value':False, 'tip':'For realistic results choose a camera calibration'}) self.pDrawViewFactor = self.pCorrViewFactor.addChild({ 'name':'Show tilt factor', 'type':'bool', 'value':False}) self.pCorrViewFactor.sigValueChanged.connect(lambda param, val: self.pDrawViewFactor.show(val)) self.pLive = pa.addChild({ 'name': 'Live', 'tip':'Update result whenever one of the parameter was changed', 'type': 'bool', 'value':False}) self.pOverrideOutput = pa.addChild({ 'name':'Override output', 'type':'bool', 'value':True}) def _setRefImg(self, display, layernumber, layername): ''' extract the reference image and -name from a given display and layer number ''' im = display.widget.image self._refImg_from_own_display = -1 self._refImg = im[layernumber] if display == self.display: self._refImg_from_own_display = layernumber self.pRefImg.setValue(layername) def _showROI(self, param, value): if self.quadROI is not None: self.quadROI.show() if value else self.quadROI.hide() def _setupQuadManually(self, param, value): # self.pSnap.show(value) if value: if not self.quadROI: self._createROI() else: self.quadROI.show() elif self.quadROI: self.quadROI.hide() def _pRefChanged(self, param, val): v = val == 'Reference image' self.pManual.show(not v) self.pRefImgChoose.show(v) # # def _pSnapChanged(self, param, val): # w = self.display.widget # # if val: # img = w.image # img = img[w.currentIndex] # # q = QuadDetection(img) # s = img.shape[0] # threshold=40 # minLineLength=int(s/10) # maxLineGap = int(s/10) # # q.findLines( threshold=threshold, # minLineLength=minLineLength, # maxLineGap=maxLineGap ) # i = np.zeros_like(img, dtype=np.uint8) # q.drawLines(i, thickness=1, color=255) # if self._cLayerLines is None: # self._cLayerLines = w.addColorLayer(layer=i, name='Detected edges', # tip='houghLines') # else: # self._cLayerLines.setLayer(i) # elif self._cLayerLines is not None: # w.removeColorLayer(self._cLayerLines) # self._cLayerLines = None def _roiMoved(self): if self._cLayerLines is not None: i = self._cLayerLines.image for h in self.quadROI.handles: #snap to closest line pos = h['item'].pos() pt = ( pos.x(), pos.y() ) closest = closestNonZeroIndex(pt, i, kSize=101) if closest is not None: h['item'].setPos(closest[0], closest[1]) self.quadROI.update() def _createROI(self): w = self.display.widget s = w.image[w.currentIndex].shape[:2] if not self.quadROI: self.quadROI = pg.PolyLineROI([[s[0]*0.2,s[1]*0.2], [s[0]*0.8, s[1]*0.2], [s[0]*0.8, s[1]*0.8], [s[0]*0.2, s[1]*0.8]], closed=True, pen='r') self.quadROI.translatable = False self.quadROI.mouseHovering = False self.quadROI.sigRegionChangeFinished.connect(self._roiMoved) #TODO: just disconnect all signals instead of having lambda #PREVENT CREATION OF SUB SEGMENTS: for s in self.quadROI.segments: s.mouseClickEvent = lambda x:None w.view.vb.addItem(self.quadROI) def activate(self): if self.pLive.value(): self._prepare() self.display.widget.item.sigImageChanged.connect( self._processAndDone) else: self.startThread(self._prepareAndProcess, self._done) def _prepare(self): w = self.display.widget img = w.image[w.currentIndex] #HOMOGRAPHY THROUGH REFERENCE IMAGE if self.pRef.value() == 'Reference image': ref = self._refImg else: #HOMOGRAPHY THROUGH QUAD if self.pManual.value(): #need to transpose because of different conventions vertices = np.array([(h['pos'].y(),h['pos'].x()) for h in self.quadROI.handles]) vertices += self.quadROI.pos() else: vertices = QuadDetection(img).vertices if not self.quadROI: self._createROI() #show found ROI: for h,c in zip(self.quadROI.handles, vertices): pos = c[::-1] - self.quadROI.pos() h['item'].setPos(pos[0], pos[1]) ref = vertices self.quadROI.show() c = self.calFileTool.currentCameraMatrix() #height/width: if self.pCalcAR.value(): w,h = None,None else: w = self.pObjWidth.value() if w == 0: w = None h = self.pObjHeight.value() if h == 0: h = None #output size: size = None if not self.pCalcOutSize.value(): size = (self.pOutHeight.value(),self.pOutWidth.value()) #INIT: self.pc = PC(img.shape, obj_width_mm=w, obj_height_mm=h, cameraMatrix=c, do_correctIntensity=self.pCorrViewFactor.value(), new_size=size ) self.pc.setReference(ref) def _process(self): w = self.display.widget img = w.image out = [] r = self.pRef.value() == 'Reference image' e = self.pExecOn.value() for n,i in enumerate(img): if (e == 'all images' or (e=='current image' and n == w.currentIndex) or (e=='last image' and n == len(img)-1) ): if not (r and n == self._refImg_from_own_display): out.append(self.pc.correct(i)) return out def _done(self, out): change = 'PerspectiveFit' if not self.outDisplay or self.outDisplay.isClosed(): self.outDisplay = self.display.workspace.addDisplay( origin=self.display, changes=change, data=out, title=change) else: self.outDisplay.widget.update(out) self.outDisplay.widget.updateView() if self.pCorrViewFactor.value() and self.pDrawViewFactor.value(): if not self.outDisplayViewFactor: self.outDisplayViewFactor = self.display.workspace.addDisplay( origin=self.display, changes=change, data=[self.pc.maps['tilt_factor']], title='Tilt factor') else: self.outDisplayViewFactor.widget.setImage(self.pc.maps['tilt_factor']) # # if self.pSceneReconstruction.value(): # #print p.scene()[0,0], p.scene()[-1,-1] # if not self.outDisplayScene: # self.outDisplayScene = self.workspace.addDisplay( # axes = 4, # origin=self.display, # changes=change, # data=[p.scene()], # title='Scene reconstruction') # else: # self.outDisplayScene.widget.update(index=0, data=p.scene()) # # self.outDisplayScene.widget.updateView(xRange=p.sceneRange()[1], # #yRange=p.sceneRange()[0] # ) if not self.pOverrideOutput.value(): self.outDisplay = None self.outDisplayViewFactor = None if not self.pLive.value(): self.setChecked(False) del self.pc # if self.pDraw3dAxis.value(): # out = p.draw3dCoordAxis() # w.item.blockSignals(True) # w.setImage(out) # w.item.blockSignals(False) # if self.axisLayer == None: # self.axisLayer = w.addColorLayer(out, name='3daxis')#, indices=found_indices) # else: # self.axisLayer.setImage(out) # if not self.outDisplayAxes: # self.outDisplayAxes = self.workspace.addDisplay( # origin=self.display, # changes=change, # data=[out], # title='Axes') # else: # self.outDisplayAxes.widget.setImage(out) def _prepareAndProcess(self): self._prepare() return self._process() def _processAndDone(self): out = self._process() self._done(out) def deactivate(self): try: w = self.display.widget w.item.sigImageChanged.disconnect(self._processAndDone) except: pass
class PerspectiveCorrection(Tool): ''' Correct the rotation and position of a quad distorted through perspective. The edge points of the quad can be chosen manually or automated ''' icon = 'quadDetection.svg' def __init__(self, imageDisplay): Tool.__init__(self, imageDisplay) self.quadROI = None self.outDisplay = None self.outDisplayViewFactor = None self._cLayerLines = None pa = self.setParameterMenu() self.calFileTool = self.showGlobalTool(CalibrationFile) self.pExecOn = pa.addChild({ 'name': 'Execute on', 'type': 'list', 'value': 'all images', 'limits': ['current image', 'all images', 'last image'] }) self.pRef = pa.addChild({ 'name': 'Reference', 'type': 'list', 'value': 'Object profile', 'limits': ['Object profile', 'Reference image'] }) #HOMOGRAPHY THROUGH QUAD self.pManual = self.pRef.addChild({ 'name': 'Manual object detection', 'type': 'bool', 'value': False }) # self.pSnap = self.pManual.addChild({ # 'name':'Snap at edges', # 'type':'bool', # 'value':False, # 'visible':False}) #HOMOGRAPHY THROUGH REF IMAGE self.pRefImgChoose = self.pRef.addChild({ 'name': 'Reference image', 'value': 'From display', 'type': 'menu', 'visible': False }) self.pRefImg = self.pRefImgChoose.addChild({ 'name': 'Chosen', 'value': '-', 'type': 'str', 'readonly': True }) self.pRefImgChoose.aboutToShow.connect( lambda menu: self.buildOtherDisplayLayersMenu( menu, self._setRefImg)) self.pRef.sigValueChanged.connect(self._pRefChanged) # self.pSnap.sigValueChanged.connect(self._pSnapChanged) self.pManual.sigValueChanged.connect(self._setupQuadManually) pShowPlane = self.pManual.addChild({ 'name': 'Show Plane', 'type': 'bool', 'value': True }) pShowPlane.sigValueChanged.connect(self._showROI) self.pCalcOutSize = pa.addChild({ 'name': 'Calculate output size', 'type': 'bool', 'value': True, 'tip': '' }) self.pOutWidth = self.pCalcOutSize.addChild({ 'name': 'Image width [px]', 'type': 'int', 'value': 1000, 'visible': False }) self.pOutHeight = self.pCalcOutSize.addChild({ 'name': 'Image height [px]', 'type': 'int', 'value': 600, 'visible': False }) self.pCalcOutSize.sigValueChanged.connect(lambda p, v: [ self.pCalcAR.show(v), self.pOutWidth.show(not v), self.pOutHeight.show(not v) ]) self.pCalcAR = pa.addChild({ 'name': 'Calculate aspect ratio', 'type': 'bool', 'value': True, 'tip': '' }) self.pObjWidth = self.pCalcAR.addChild({ 'name': 'Object width [mm]', 'type': 'float', 'value': 0, 'visible': False }) self.pObjHeight = self.pCalcAR.addChild({ 'name': 'Object height [mm]', 'type': 'float', 'value': 0, 'visible': False }) self.pCalcAR.sigValueChanged.connect( lambda param, val: [ch.show(not val) for ch in param.childs]) self.pCorrViewFactor = pa.addChild({ 'name': 'Correct Intensity', 'type': 'bool', 'value': False, 'tip': 'For realistic results choose a camera calibration' }) self.pDrawViewFactor = self.pCorrViewFactor.addChild({ 'name': 'Show tilt factor', 'type': 'bool', 'value': False }) self.pCorrViewFactor.sigValueChanged.connect( lambda param, val: self.pDrawViewFactor.show(val)) self.pLive = pa.addChild({ 'name': 'Live', 'tip': 'Update result whenever one of the parameter was changed', 'type': 'bool', 'value': False }) self.pOverrideOutput = pa.addChild({ 'name': 'Override output', 'type': 'bool', 'value': True }) def _setRefImg(self, display, layernumber, layername): ''' extract the reference image and -name from a given display and layer number ''' im = display.widget.image self._refImg_from_own_display = -1 self._refImg = im[layernumber] if display == self.display: self._refImg_from_own_display = layernumber self.pRefImg.setValue(layername) def _showROI(self, param, value): if self.quadROI is not None: self.quadROI.show() if value else self.quadROI.hide() def _setupQuadManually(self, param, value): # self.pSnap.show(value) if value: if not self.quadROI: self._createROI() else: self.quadROI.show() elif self.quadROI: self.quadROI.hide() def _pRefChanged(self, param, val): v = val == 'Reference image' self.pManual.show(not v) self.pRefImgChoose.show(v) # # def _pSnapChanged(self, param, val): # w = self.display.widget # # if val: # img = w.image # img = img[w.currentIndex] # # q = QuadDetection(img) # s = img.shape[0] # threshold=40 # minLineLength=int(s/10) # maxLineGap = int(s/10) # # q.findLines( threshold=threshold, # minLineLength=minLineLength, # maxLineGap=maxLineGap ) # i = np.zeros_like(img, dtype=np.uint8) # q.drawLines(i, thickness=1, color=255) # if self._cLayerLines is None: # self._cLayerLines = w.addColorLayer(layer=i, name='Detected edges', # tip='houghLines') # else: # self._cLayerLines.setLayer(i) # elif self._cLayerLines is not None: # w.removeColorLayer(self._cLayerLines) # self._cLayerLines = None def _roiMoved(self): if self._cLayerLines is not None: i = self._cLayerLines.image for h in self.quadROI.handles: #snap to closest line pos = h['item'].pos() pt = (pos.x(), pos.y()) closest = closestNonZeroIndex(pt, i, kSize=101) if closest is not None: h['item'].setPos(closest[0], closest[1]) self.quadROI.update() def _createROI(self): w = self.display.widget s = w.image[w.currentIndex].shape[:2] if not self.quadROI: self.quadROI = pg.PolyLineROI( [[s[0] * 0.2, s[1] * 0.2], [s[0] * 0.8, s[1] * 0.2], [s[0] * 0.8, s[1] * 0.8], [s[0] * 0.2, s[1] * 0.8]], closed=True, pen='r') self.quadROI.translatable = False self.quadROI.mouseHovering = False self.quadROI.sigRegionChangeFinished.connect(self._roiMoved) #TODO: just disconnect all signals instead of having lambda #PREVENT CREATION OF SUB SEGMENTS: for s in self.quadROI.segments: s.mouseClickEvent = lambda x: None w.view.vb.addItem(self.quadROI) def activate(self): if self.pLive.value(): self._prepare() self.display.widget.item.sigImageChanged.connect( self._processAndDone) else: self.startThread(self._prepareAndProcess, self._done) def _prepare(self): w = self.display.widget img = w.image[w.currentIndex] #HOMOGRAPHY THROUGH REFERENCE IMAGE if self.pRef.value() == 'Reference image': ref = self._refImg else: #HOMOGRAPHY THROUGH QUAD if self.pManual.value(): #need to transpose because of different conventions vertices = np.array([(h['pos'].y(), h['pos'].x()) for h in self.quadROI.handles]) vertices += self.quadROI.pos() else: vertices = QuadDetection(img).vertices if not self.quadROI: self._createROI() #show found ROI: for h, c in zip(self.quadROI.handles, vertices): pos = c[::-1] - self.quadROI.pos() h['item'].setPos(pos[0], pos[1]) ref = vertices self.quadROI.show() try: c = self.calFileTool.currentCameraMatrix() except TypeError: c = None #height/width: if self.pCalcAR.value(): w, h = None, None else: w = self.pObjWidth.value() if w == 0: w = None h = self.pObjHeight.value() if h == 0: h = None #output size: size = None if not self.pCalcOutSize.value(): size = (self.pOutHeight.value(), self.pOutWidth.value()) #INIT: self.pc = PC(img.shape, obj_width_mm=w, obj_height_mm=h, cameraMatrix=c, do_correctIntensity=self.pCorrViewFactor.value(), new_size=size) self.pc.setReference(ref) def _process(self): w = self.display.widget img = w.image out = [] r = self.pRef.value() == 'Reference image' e = self.pExecOn.value() for n, i in enumerate(img): if (e == 'all images' or (e == 'current image' and n == w.currentIndex) or (e == 'last image' and n == len(img) - 1)): if not (r and n == self._refImg_from_own_display): out.append(self.pc.correct(i)) return out def _done(self, out): change = 'PerspectiveFit' if not self.outDisplay or self.outDisplay.isClosed(): self.outDisplay = self.display.workspace.addDisplay( origin=self.display, changes=change, data=out, title=change) else: self.outDisplay.widget.update(out) self.outDisplay.widget.updateView() if self.pCorrViewFactor.value() and self.pDrawViewFactor.value(): if not self.outDisplayViewFactor: self.outDisplayViewFactor = self.display.workspace.addDisplay( origin=self.display, changes=change, data=[self.pc.maps['tilt_factor']], title='Tilt factor') else: self.outDisplayViewFactor.widget.setImage( self.pc.maps['tilt_factor']) # # if self.pSceneReconstruction.value(): # #print p.scene()[0,0], p.scene()[-1,-1] # if not self.outDisplayScene: # self.outDisplayScene = self.workspace.addDisplay( # axes = 4, # origin=self.display, # changes=change, # data=[p.scene()], # title='Scene reconstruction') # else: # self.outDisplayScene.widget.update(index=0, data=p.scene()) # # self.outDisplayScene.widget.updateView(xRange=p.sceneRange()[1], # #yRange=p.sceneRange()[0] # ) if not self.pOverrideOutput.value(): self.outDisplay = None self.outDisplayViewFactor = None if not self.pLive.value(): self.setChecked(False) del self.pc # if self.pDraw3dAxis.value(): # out = p.draw3dCoordAxis() # w.item.blockSignals(True) # w.setImage(out) # w.item.blockSignals(False) # if self.axisLayer == None: # self.axisLayer = w.addColorLayer(out, name='3daxis')#, indices=found_indices) # else: # self.axisLayer.setImage(out) # if not self.outDisplayAxes: # self.outDisplayAxes = self.workspace.addDisplay( # origin=self.display, # changes=change, # data=[out], # title='Axes') # else: # self.outDisplayAxes.widget.setImage(out) def _prepareAndProcess(self): self._prepare() return self._process() def _processAndDone(self): out = self._process() self._done(out) def deactivate(self): try: w = self.display.widget w.item.sigImageChanged.disconnect(self._processAndDone) except: pass