def setGlobalPosition(self, pos, speed='fast'): """Move the microscope such that its center axis is at a specified global position. If *pos* is a 3-element vector, then this method will also attempt to set the focus depth accordingly. Return a MoveFuture instance. Note: If the xy positioning device is different from the z positioning device, then the MoveFuture returned only corresponds to the xy motion. """ pd = self.positionDevice() fd = self.focusDevice() if len(pos) == 3 and fd is not pd: z = pos[2] self.setFocusDepth(z) pos = pos[:2] if len(pos) == 2: pos = list(pos) + [self.getFocusDepth()] # Determine how to move the xy(z) stage to react the new center position gpos = self.globalPosition() sgpos = pd.globalPosition() sgpos2 = pg.Vector(sgpos) + (pg.Vector(pos) - gpos) sgpos2 = [sgpos2.x(), sgpos2.y(), sgpos2.z()] return pd.moveToGlobal(sgpos2, speed)
def __init__(self, config, scope, key): #self.__sigProxy = Objective.SignalProxyObject() #self.sigTransformChanged = self.__sigProxy.sigTransformChanged #self._config = config self._config = config self._scope = scope self._key = key offset = config.get('offset', pg.Vector(0, 0, 0)) scale = config.get('scale', pg.Vector(1, 1, 1)) name = config['name'] OptomechDevice.__init__(self, scope.dm, {}, name) if 'offset' in config: self.setOffset(config['offset']) if 'scale' in config: self.setScale(config['scale'])
def measureTipPosition(self, padding=50e-6, threshold=0.7, frame=None, pos=None, tipLength=None, show=False): """Find the pipette tip location by template matching within a region surrounding the expected tip position. Return `((x, y, z), corr)`, where *corr* is the normalized cross-correlation value of the best template match. If the strength of the match is less than *threshold*, then raise RuntimeError. """ # Grab one frame (if it is not already supplied) and crop it to the region around the pipette tip. if frame is None: frame = self.takeFrame() # load up template images reference = self._getReference() if tipLength is None: # select a tip length similar to template images tipLength = reference['tipLength'] minImgPos, maxImgPos, tipRelPos = self.getTipImageArea(frame, padding, pos=pos, tipLength=tipLength) img = frame.data() if img.ndim == 3: img = img[0] img = img[minImgPos[0]:maxImgPos[0], minImgPos[1]:maxImgPos[1]] img = self.filterImage(img) # resample acquired image to match template pixel size pxr = frame.info()['pixelSize'][0] / reference['pixelSize'][0] if pxr != 1.0: img = scipy.ndimage.zoom(img, pxr) # run template match against all template frames, find the frame with the strongest match match = [self.matchTemplate(img, t) for t in reference['frames']] if show: pg.plot([m[0][0] for m in match], title='x match vs z') pg.plot([m[0][1] for m in match], title='y match vs z') pg.plot([m[1] for m in match], title='match correlation vs z') maxInd = np.argmax([m[1] for m in match]) if match[maxInd][1] < threshold: raise RuntimeError("Unable to locate pipette tip (correlation %0.2f < %0.2f)" % (match[maxInd][1], threshold)) # measure z error zErr = (maxInd - reference['centerInd']) * reference['zStep'] # measure xy position offset = match[maxInd][0] tipImgPos = (minImgPos[0] + (offset[0] + reference['centerPos'][0]) / pxr, minImgPos[1] + (offset[1] + reference['centerPos'][1]) / pxr) tipPos = frame.mapFromFrameToGlobal(pg.Vector(tipImgPos)) return (tipPos.x(), tipPos.y(), tipPos.z() + zErr), match[maxInd][1]
def autoCalibrate(self, **kwds): """Automatically calibrate the pipette tip position using template matching on a single camera frame. Return the offset in pipette-local coordinates and the normalized cross-correlation value of the template match. All keyword arguments are passed to `measureTipPosition()`. """ # If no image padding is given, then use the template tip length as a first guess if 'padding' not in kwds: ref = self._getReference() kwds['padding'] = ref['tipLength'] try: tipPos, corr = self.measureTipPosition(**kwds) except RuntimeError: kwds['padding'] *= 2 tipPos, corr = self.measureTipPosition(**kwds) localError = self.dev.mapFromGlobal(tipPos) tr = self.dev.deviceTransform() tr.translate(pg.Vector(localError)) self.dev.setDeviceTransform(tr) return localError, corr
def getBackground(self): if self.background is None: w, h = self.params['sensorSize'] tr = self.globalTransform() if isinstance(self.bgData, dict): # select data based on objective obj = self.getObjective() data = self.bgData[obj] info = self.bgInfo[obj] px = info['pixelSize'] pz = info['depths'][1] - info['depths'][0] m = pg.QtGui.QMatrix4x4() pos = info['transform']['pos'] m.scale(1 / px[0], 1 / px[1], 1 / pz) m.translate(-pos[0], -pos[1], -info['depths'][0]) tr2 = m * tr origin = tr2.map(pg.Vector(0, 0, 0)) #print(origin) origin = [int(origin.x()), int(origin.y()), origin.z()] ## slice data camRect = QtCore.QRect(origin[0], origin[1], w, h) dataRect = QtCore.QRect(0, 0, data.shape[1], data.shape[2]) overlap = camRect.intersected(dataRect) tl = overlap.topLeft() - camRect.topLeft() z = origin[2] z1 = np.floor(z) z2 = np.ceil(z) s = (z - z1) / (z2 - z1) z1 = int(np.clip(z1, 0, data.shape[0] - 1)) z2 = int(np.clip(z2, 0, data.shape[0] - 1)) src1 = data[z1, overlap.left():overlap.left() + overlap.width(), overlap.top():overlap.top() + overlap.height()] src2 = data[z2, overlap.left():overlap.left() + overlap.width(), overlap.top():overlap.top() + overlap.height()] src = src1 * (1 - s) + src2 * s bg = np.empty((w, h), dtype=data.dtype) bg[:] = 100 bg[tl.x():tl.x() + overlap.width(), tl.y():tl.y() + overlap.height()] = src self.background = bg #vectors = ([1, 0, 0], [0, 1, 0]) #self.background = pg.affineSlice(data, (w,h), origin, vectors, (1, 2, 0), order=1) else: tr = pg.SRTTransform(tr) m = QtGui.QTransform() m.scale(3e6, 3e6) m.translate(0.0005, 0.0005) tr = tr * m origin = tr.map(pg.Point(0, 0)) x = (tr.map(pg.Point(1, 0)) - origin) y = (tr.map(pg.Point(0, 1)) - origin) origin = np.array([origin.x(), origin.y()]) x = np.array([x.x(), x.y()]) y = np.array([y.x(), y.y()]) ## slice fractal from pre-rendered data vectors = (x, y) self.background = pg.affineSlice(self.bgData, (w, h), origin, vectors, (0, 1), order=1) return self.background
def getTipImageArea(self, frame, padding, pos=None, tipLength=None): """Generate coordinates needed to clip a camera frame to include just the tip of the pipette and some padding. By default, images will include the tip of the pipette to a length of 100 pixels. Return a tuple (minImgPos, maxImgPos, tipRelPos), where the first two items are (x,y) coordinate pairs giving the corners of the image region to be extracted, and tipRelPos is the subpixel location of the pipette tip within this region. """ img = frame.data() if img.ndim == 3: img = img[0] if tipLength is None: tipLength = self.suggestTipLength(frame) # determine bounding rectangle that we would like to acquire from the tip if pos is not None: tipPos = pos else: tipPos = self.dev.globalPosition() tipPos = np.array([tipPos[0], tipPos[1]]) angle = self.dev.getYawAngle() * np.pi / 180. da = 10 * np.pi / 180 # half-angle of the tip pxw = frame.info()['pixelSize'][0] # compute back points of a triangle that circumscribes the tip backPos1 = np.array( [-tipLength * np.cos(angle + da), -tipLength * np.sin(angle + da)]) backPos2 = np.array( [-tipLength * np.cos(angle - da), -tipLength * np.sin(angle - da)]) # convert to image coordinates tr = frame.globalTransform().inverted()[0] originImgPos = tr.map(pg.Vector([0, 0])) backImgPos1 = tr.map(pg.Vector(backPos1)) - originImgPos backImgPos2 = tr.map(pg.Vector(backPos2)) - originImgPos backImgPos1 = np.array([backImgPos1.x(), backImgPos1.y()]) backImgPos2 = np.array([backImgPos2.x(), backImgPos2.y()]) # Pixel positions of bounding corners in the image relative to tip, including padding. # Note this is all calculated without actual tip position; this ensures the image # size is constant even as the tip moves. allPos = np.vstack([[0, 0], backImgPos1, backImgPos2]).astype('int') padding = int(padding / pxw) minRelPos = allPos.min(axis=0) - padding maxRelPos = allPos.max(axis=0) + padding # Get absolute pixel position of tip within image tipImgPos = tr.map(pg.Vector(tipPos)) tipImgPos = np.array([tipImgPos.x(), tipImgPos.y()]) tipImgPx = tipImgPos.astype('int') # clip bounding coordinates minRelPos = [ np.clip(minRelPos[0], -tipImgPx[0], img.shape[0] - 1 - tipImgPx[0]), np.clip(minRelPos[1], -tipImgPx[1], img.shape[1] - 1 - tipImgPx[1]) ] maxRelPos = [ np.clip(maxRelPos[0], -tipImgPx[0], img.shape[0] - 1 - tipImgPx[0]), np.clip(maxRelPos[1], -tipImgPx[1], img.shape[1] - 1 - tipImgPx[1]) ] # absolute image coordinates of bounding rect minImgPos = tipImgPx + minRelPos maxImgPos = tipImgPx + maxRelPos if np.any(maxImgPos - minImgPos < 1): raise RuntimeError("No part of tip overlaps with camera frame.") # subpixel location of tip within image tipRelPos = tipImgPos - tipImgPx - minRelPos return minImgPos, maxImgPos, tipRelPos
def mapErrors(self, nSteps=(5, 5, 7), stepSize=(50e-6, 50e-6, 50e-6), padding=60e-6, threshold=0.4, speed='slow', show=False, intermediateDist=60e-6): """Move pipette tip randomly to locations in a grid and measure the position error at each location. All tip locations must be within the field of view. """ startTime = time.time() start = np.array(self.dev.globalPosition()) npts = nSteps[0] * nSteps[1] * nSteps[2] inds = np.mgrid[0:nSteps[0], 0:nSteps[1], 0:nSteps[2]].reshape( (3, npts)).transpose() order = np.arange(npts) np.random.shuffle(order) err = np.zeros(nSteps + (3, )) stepSize = np.array(stepSize) if show: imv = pg.image() mark1 = pg.QtGui.QGraphicsEllipseItem( pg.QtCore.QRectF(-5, -5, 10, 10)) mark1.setBrush(pg.mkBrush(255, 255, 0, 100)) mark1.setZValue(100) imv.addItem(mark1) mark2 = pg.QtGui.QGraphicsEllipseItem( pg.QtCore.QRectF(-5, -5, 10, 10)) mark2.setBrush(pg.mkBrush(255, 0, 0, 100)) mark2.setZValue(100) imv.addItem(mark2) # loop over all points in random order, and such that we do heavy computation while # pipette is moving. images = [] offsets = [] try: with pg.ProgressDialog("Acquiring error map...", 0, len(order)) as dlg: for i in range(len(order) + 1): if i > 0: lastPos = pos if i < len(order): ind = inds[order[i]] pos = start.copy() + (stepSize * ind) # Jump to position + a random 20um offset to avoid hysteresis offset = np.random.normal(size=3) offset *= intermediateDist / (offset**2).sum()**0.5 offsets.append(offset) mfut = self.dev._moveToGlobal(pos + offset, speed) ffut = self.dev.scopeDevice().setFocusDepth( pos[2], speed) if i > 0: ind = inds[order[i - 1]] print("Frame: %d %s" % (i - 1, lastPos)) err[tuple(ind)] = self.measureError( padding=padding, threshold=threshold, frame=frame, pos=lastPos) print(" error: %s" % err[tuple(ind)]) dlg += 1 if show: imv.setImage(frame.data()[0]) p1 = frame.globalTransform().inverted()[0].map( pg.Vector(lastPos)) p2 = frame.globalTransform().inverted()[0].map( pg.Vector(lastPos + err[tuple(ind)])) mark1.setPos(p1.x(), p1.y()) mark2.setPos(p2.x(), p2.y()) # wait for previous moves to complete mfut.wait(updates=True) ffut.wait(updates=True) # step back to actual target position self.dev._moveToGlobal(pos, speed).wait(updates=True) frame = self.takeFrame() if dlg.wasCanceled(): return None finally: self.dev._moveToGlobal(start, 'fast') self.dev.scopeDevice().setFocusDepth(start[2], 'fast') self.errorMap = { 'err': err, 'nSteps': nSteps, 'stepSize': stepSize, 'order': order, 'inds': inds, 'offsets': offsets, 'time': time.time() - startTime, } filename = self.dev.configFileName('error_map.np') np.save(open(filename, 'wb'), self.errorMap) return self.errorMap