예제 #1
0
    def _on_ensemble_test(self, event=None):
        from PYME.recipes.base import ModuleCollection
        from PYME.IO.FileUtils import nameUtils
        from nep_fitting.recipe_modules import nep_fits
        from nep_fitting import reports
        from PYME.IO.ragged import RaggedCache
        import webbrowser

        rec = ModuleCollection()

        rec.add_module(
            nep_fits.TestEnsembleParameters(
                rec,
                inputName='line_profiles',
                fit_type=list(profile_fitters.ensemble_fitters.keys())[0],
                hold_ensemble_parameter_constant=False,
                outputName='output'))

        # populate namespace with current profiles
        rec.namespace['line_profiles'] = RaggedCache(
            self._line_profile_handler.get_line_profiles())
        if not rec.configure_traits(view=rec.pipeline_view, kind='modal'):
            return  # handle cancel

        res = rec.execute()

        # generate report
        context = {
            #'ensemble_parameters': res.mdh['TestEnsembleParameters.EnsembleTestValues'],  # note this is a dict
            'results':
            res,
            'filename':
            self._dsviewer.image.filename,
            'fittype':
            res.mdh['TestEnsembleParameters.FitType'],
            'img_schematic':
            reports.img_as_strb64(
                reports.get_schematic(
                    res.mdh['TestEnsembleParameters.FitType']))
        }
        handler_names = self._line_profile_handler.get_image_names()
        if (len(handler_names) == 1) and (handler_names[0] == self.image_name):
            # if there's only a single image, include it in the report
            context['img_data'] = reports.img_as_strb64(
                self._dsviewer.view.GrabPNGToBuffer())

        fdialog = wx.FileDialog(
            None,
            'Save report as ...',
            wildcard='html (*.html)|*.html',
            style=wx.FD_SAVE,
            defaultDir=nameUtils.genShiftFieldDirectoryPath())
        succ = fdialog.ShowModal()
        if (succ == wx.ID_OK):
            fpath = os.path.splitext(fdialog.GetPath())[0] + '.html'
            reports.generate_and_save(fpath,
                                      context,
                                      template_name='ensemble_test.html')

            webbrowser.open('file://' + fpath, 2)
예제 #2
0
    def OnGenShiftmapQuad(self, event):
        from PYME.Analysis.points import twoColour, twoColourPlot
        from PYME.IO.MetaDataHandler import get_camera_roi_origin

        pipeline = self.visFr.pipeline

        vs = pipeline.mdh.voxelsize_nm
        
        roi_x0, roi_y0 = get_camera_roi_origin(pipeline.mdh)
        
        x0 = (roi_x0)*vs[0]
        y0 = (roi_y0)*vs[1]
        
        lx = len(pipeline.filter['x'])
        bbox = None#[0,(pipeline.mdh['Camera.ROIWidth'] + 1)*vs[0], 0,(pipeline.mdh['Camera.ROIHeight'] + 1)*vs[1]]
        dx, dy, spx, spy, good = twoColour.genShiftVectorFieldQ(pipeline.filter['x']+.1*np.random.randn(lx) + x0, pipeline.filter['y']+.1*np.random.randn(lx) + y0, pipeline.filter['fitResults_dx'], pipeline.filter['fitResults_dy'], pipeline.filter['fitError_dx'], pipeline.filter['fitError_dy'], bbox=bbox)
        #twoColourPlot.PlotShiftField(dx, dy, spx, spy)
        twoColourPlot.PlotShiftField2(spx, spy, pipeline.mdh['Splitter.Channel0ROI'][2:], voxelsize=vs)
        twoColourPlot.PlotShiftResiduals(pipeline['x'][good] + x0, pipeline['y'][good] + y0, pipeline['fitResults_dx'][good], pipeline['fitResults_dy'][good], spx, spy)

        from six.moves import cPickle

        defFile = os.path.splitext(os.path.split(self.visFr.GetTitle())[-1])[0] + '.sf'

        fdialog = wx.FileDialog(None, 'Save shift field as ...',
            wildcard='Shift Field file (*.sf)|*.sf', style=wx.FD_SAVE, defaultDir = nameUtils.genShiftFieldDirectoryPath(), defaultFile=defFile)
        succ = fdialog.ShowModal()
        if (succ == wx.ID_OK):
            fpath = fdialog.GetPath()
            #save as a pickle containing the data and voxelsize

            fid = open(fpath, 'wb')
            cPickle.dump((spx, spy), fid, 2)
            fid.close()
예제 #3
0
 def OnSetShiftField(self, event):
     fdialog = wx.FileDialog(None, 'Select shift field',
         wildcard='*.sf', style=wx.FD_OPEN, defaultDir = nameUtils.genShiftFieldDirectoryPath())
     succ = fdialog.ShowModal()
     if (succ == wx.ID_OK):
         sfname = fdialog.GetPath()
         
         self.SetShiftField(sfname)
예제 #4
0
    def OnGenShiftmapQuadz(self, event):
        from PYME.Analysis.points import twoColour, twoColourPlot

        pipeline = self.visFr.pipeline

        vs = [
            pipeline.mdh['voxelsize.x'] * 1e3,
            pipeline.mdh['voxelsize.y'] * 1e3, 200.
        ]

        z0 = (pipeline['z'] * pipeline['A']).sum() / pipeline['A'].sum()

        lx = len(pipeline.filter['x'])
        bbox = None  #[0,(pipeline.mdh['Camera.ROIWidth'] + 1)*vs[0], 0,(pipeline.mdh['Camera.ROIHeight'] + 1)*vs[1]]
        dx, dy, spx, spy, good = twoColour.genShiftVectorFieldQz(
            pipeline.filter['x'] + .1 * pylab.randn(lx),
            pipeline.filter['y'] + .1 * pylab.randn(lx),
            pipeline.filter['z'] - z0,
            pipeline.filter['fitResults_dx'],
            pipeline.filter['fitResults_dy'],
            pipeline.filter['fitError_dx'],
            pipeline.filter['fitError_dy'],
            bbox=bbox)
        #twoColourPlot.PlotShiftField(dx, dy, spx, spy)
        twoColourPlot.PlotShiftField2(spx,
                                      spy,
                                      pipeline.mdh['Splitter.Channel0ROI'][2:],
                                      voxelsize=vs)
        twoColourPlot.PlotShiftResiduals(pipeline['x'][good],
                                         pipeline['y'][good],
                                         pipeline['fitResults_dx'][good],
                                         pipeline['fitResults_dy'][good], spx,
                                         spy, pipeline.filter['z'][good] - z0)

        from six.moves import cPickle

        defFile = os.path.splitext(os.path.split(
            self.visFr.GetTitle())[-1])[0] + '.sf'

        fdialog = wx.FileDialog(
            None,
            'Save shift field as ...',
            wildcard='Shift Field file (*.sf)|*.sf',
            style=wx.FD_SAVE,
            defaultDir=nameUtils.genShiftFieldDirectoryPath(),
            defaultFile=defFile)
        succ = fdialog.ShowModal()
        if (succ == wx.ID_OK):
            fpath = fdialog.GetPath()
            #save as a pickle containing the data and voxelsize

            fid = open(fpath, 'wb')
            cPickle.dump((spx, spy), fid, 2)
            fid.close()
예제 #5
0
    def OnCalibrateShifts(self, event):
        """

        Generates multiview shiftmaps on bead-data. Only beads which show up in all channels are
        used to generate the shiftmap.

        Parameters
        ----------

            searchRadius: Radius within which to clump bead localizations that appear in all channels. Units of pixels.

        Notes
        -----

        """
        from PYME.recipes.multiview import CalibrateShifts

        # recipe = self.pipeline.recipe
        # # hold off auto-running the recipe until we configure things
        # recipe.trait_set(execute_on_invalidation=False)
        # try:
        #     calibration_module = CalibrateShifts(recipe, input_name=self.pipeline.selectedDataSourceKey,
        #                                      output_name='shiftmap')
        #
        #     recipe.add_module(calibration_module)
        #     if not recipe.configure_traits(view=recipe.pipeline_view, kind='modal'):
        #         return
        #
        #     recipe.execute()
        #     sm = recipe.namespace['shiftmap']
        # finally:  # make sure that we configure the pipeline recipe as it was
        #     recipe.trait_set(execute_on_invalidation=True)


        calibration_module = CalibrateShifts()
        if not calibration_module.configure_traits(kind='modal'):
            return
        
        sm = calibration_module.apply_simple(self.pipeline.selectedDataSource)


        # save the file
        defFile = os.path.splitext(os.path.split(self.pipeline.filename)[-1])[0] + 'MultiView.sf'

        fdialog = wx.FileDialog(None, 'Save shift field as ...',
                                wildcard='Shift Field file (*.sf)|*.sf', style=wx.FD_SAVE,
                                defaultDir=nameUtils.genShiftFieldDirectoryPath(), defaultFile=defFile)
        succ = fdialog.ShowModal()
        if (succ == wx.ID_OK):
            fpath = fdialog.GetPath()

            sm.to_hdf(fpath, tablename='shift_map', metadata=sm.mdh)
    def OnShiftCorrectFolded(self, event=None):
        """
        Applies chromatic shift correction to folded localization data that was acquired with an
        image splitting device, but localized without splitter awareness.
        See recipes.localizations.MultiviewShiftCorrect.

        Parameters
        ----------

            None

        Notes
        -----

        """

        from PYME.recipes.multiview import ShiftCorrect
        pipeline = self.pipeline
        recipe = self.pipeline.recipe

        if 'FIXMESiftmap' in pipeline.mdh.keys(
        ):  # load shiftmaps from metadata, if present
            fpath = pipeline.mdh['FIXMEShiftmap']  #FIXME: break this for now
        else:
            fdialog = wx.FileDialog(
                None,
                'Load shift field',
                wildcard='Shift Field file (*.sf)|*.sf',
                style=wx.FD_OPEN,
                defaultDir=nameUtils.genShiftFieldDirectoryPath())
            succ = fdialog.ShowModal()

            if (succ == wx.ID_OK):
                fpath = fdialog.GetPath()
            else:
                raise RuntimeError(
                    'Shiftmaps not found in metadata and could not be loaded from file'
                )

        recipe.add_module(
            ShiftCorrect(recipe,
                         input_name=pipeline.selectedDataSourceKey,
                         shift_map_path=fpath,
                         output_name='shift_corrected'))
        recipe.execute()
        self.pipeline.selectDataSource('shift_corrected')
예제 #7
0
 def OnSetShiftField(self, event=None):
     from PYME.IO.FileUtils import nameUtils
     fdialog = wx.FileDialog(
         None,
         'Please select shift field to use ...',
         wildcard='Shift fields|*.sf',
         style=wx.FD_OPEN,
         defaultDir=nameUtils.genShiftFieldDirectoryPath())
     succ = fdialog.ShowModal()
     if (succ == wx.ID_OK):
         #self.ds = example.CDataStack(fdialog.GetPath().encode())
         #self.ds =
         sfFilename = fdialog.GetPath()
         self.image.mdh.setEntry('chroma.ShiftFilename', sfFilename)
         dx, dy = np.load(sfFilename)
         self.image.mdh.setEntry('chroma.dx', dx)
         self.image.mdh.setEntry('chroma.dy', dy)
         #self.md.setEntry('PSFFile', psfFilename)
         #self.stShiftFieldName.SetLabel('Shifts: %s' % os.path.split(sfFilename)[1])
         #self.stShiftFieldName.SetForegroundColour(wx.Colour(0, 128, 0))
         return True
     else:
         return False
예제 #8
0
    def OnCalibrateAstigmatism(self, event):
        #TODO - move all non-GUI logic for this out of this file?
        from PYME.recipes.measurement import FitPoints
        from PYME.IO.FileUtils import nameUtils
        import matplotlib.pyplot as plt
        import mpld3
        import json
        from PYME.Analysis.PSFEst import extractImages
        import wx
        from PYME.Analysis.points.astigmatism import astigTools

        # query user for type of calibration
        # NB - GPU fit is not enabled here because it exits on number of iterations, which is not necessarily convergence for very bright beads!
        ftypes = ['BeadConvolvedAstigGaussFit',
                  'AstigGaussFitFR']  # , 'AstigGaussGPUFitFR']
        fitType_dlg = wx.SingleChoiceDialog(self.dsviewer,
                                            'Fit-type selection',
                                            'Fit-type selection', ftypes)
        fitType_dlg.ShowModal()
        fitMod = ftypes[fitType_dlg.GetSelection()]

        if (fitMod == 'BeadConvolvedAstigGaussFit') and (
                'Bead.Diameter' not in self.image.mdh.keys()):
            beadDiam_dlg = wx.NumberEntryDialog(None, 'Bead diameter in nm',
                                                'diameter [nm]',
                                                'diameter [nm]', 100, 1, 9e9)
            beadDiam_dlg.ShowModal()
            beadDiam = float(beadDiam_dlg.GetValue())
            # store this in metadata
            self.image.mdh['Analysis.Bead.Diameter'] = beadDiam

        ps = self.image.pixelSize

        obj_positions = {}

        obj_positions['x'] = ps * self.image.data.shape[0] * 0.5 * np.ones(
            self.image.data.shape[2])
        obj_positions['y'] = ps * self.image.data.shape[1] * 0.5 * np.ones(
            self.image.data.shape[2])
        obj_positions['t'] = np.arange(self.image.data.shape[2])
        z = np.arange(
            self.image.data.shape[2]) * self.image.mdh['voxelsize.z'] * 1.e3
        obj_positions['z'] = z - z.mean()

        ptFitter = FitPoints()
        ptFitter.trait_set(roiHalfSize=11)
        ptFitter.trait_set(fitModule=fitMod)

        namespace = {'input': self.image, 'objPositions': obj_positions}

        results = []

        for chanNum in range(self.image.data.shape[3]):
            # get z centers
            dx, dy, dz = extractImages.getIntCenter(self.image.data[:, :, :,
                                                                    chanNum])

            ptFitter.trait_set(channel=chanNum)
            ptFitter.execute(namespace)

            res = namespace['fitResults']

            dsigma = abs(res['fitResults_sigmax']) - abs(
                res['fitResults_sigmay'])
            valid = ((res['fitError_sigmax'] > 0) *
                     (res['fitError_sigmax'] < 50) *
                     (res['fitError_sigmay'] < 50) *
                     (res['fitResults_A'] > 0) > 0)

            results.append({
                'sigmax':
                abs(res['fitResults_sigmax'][valid]).tolist(),
                'error_sigmax':
                abs(res['fitError_sigmax'][valid]).tolist(),
                'sigmay':
                abs(res['fitResults_sigmay'][valid]).tolist(),
                'error_sigmay':
                abs(res['fitError_sigmay'][valid]).tolist(),
                'dsigma':
                dsigma[valid].tolist(),
                'z':
                obj_positions['z'][valid].tolist(),
                'zCenter':
                obj_positions['z'][int(dz)]
            })

        #generate new tab to show results
        use_web_view = True
        if not '_astig_view' in dir(self):
            try:
                self._astig_view = wx.html2.WebView.New(self.dsviewer)
                self.dsviewer.AddPage(self._astig_view, True,
                                      'Astigmatic calibration')

            except NotImplementedError:
                use_web_view = False

        # find reasonable z range for each channel, inject 'zRange' into the results. FIXME - injection is bad
        results = astigTools.find_and_add_zRange(results)

        #do plotting
        plt.ioff()
        f = plt.figure(figsize=(10, 4))

        colors = iter(
            plt.cm.Dark2(np.linspace(0, 1, 2 * self.image.data.shape[3])))
        plt.subplot(121)
        for i, res in enumerate(results):
            nextColor1 = next(colors)
            nextColor2 = next(colors)
            lbz = np.absolute(res['z'] - res['zRange'][0]).argmin()
            ubz = np.absolute(res['z'] - res['zRange'][1]).argmin()
            plt.plot(res['z'], res['sigmax'], ':',
                     c=nextColor1)  # , label='x - %d' % i)
            plt.plot(res['z'], res['sigmay'], ':',
                     c=nextColor2)  # , label='y - %d' % i)
            plt.plot(res['z'][lbz:ubz],
                     res['sigmax'][lbz:ubz],
                     label='x - %d' % i,
                     c=nextColor1)
            plt.plot(res['z'][lbz:ubz],
                     res['sigmay'][lbz:ubz],
                     label='y - %d' % i,
                     c=nextColor2)

        #plt.ylim(-200, 400)
        plt.grid()
        plt.xlabel('z position [nm]')
        plt.ylabel('Sigma [nm]')
        plt.legend()

        plt.subplot(122)
        colors = iter(plt.cm.Dark2(np.linspace(0, 1,
                                               self.image.data.shape[3])))
        for i, res in enumerate(results):
            nextColor = next(colors)
            lbz = np.absolute(res['z'] - res['zRange'][0]).argmin()
            ubz = np.absolute(res['z'] - res['zRange'][1]).argmin()
            plt.plot(res['z'], res['dsigma'], ':', lw=2,
                     c=nextColor)  # , label='Chan %d' % i)
            plt.plot(res['z'][lbz:ubz],
                     res['dsigma'][lbz:ubz],
                     lw=2,
                     label='Chan %d' % i,
                     c=nextColor)
        plt.grid()
        plt.xlabel('z position [nm]')
        plt.ylabel('Sigma x - Sigma y [nm]')
        plt.legend()

        plt.tight_layout()

        plt.ion()
        #dat = {'z' : objPositions['z'][valid].tolist(), 'sigmax' : res['fitResults_sigmax'][valid].tolist(),
        #                   'sigmay' : res['fitResults_sigmay'][valid].tolist(), 'dsigma' : dsigma[valid].tolist()}

        if use_web_view:
            fig = mpld3.fig_to_html(f)
            data = json.dumps(results)

            template = env.get_template('astigCal.html')
            html = template.render(astigplot=fig, data=data)
            #print html
            self._astig_view.SetPage(html, '')
        else:
            plt.show()

        fdialog = wx.FileDialog(
            None,
            'Save Astigmatism Calibration as ...',
            wildcard='Astigmatism Map (*.am)|*.am',
            style=wx.FD_SAVE,
            defaultDir=nameUtils.genShiftFieldDirectoryPath(
            ))  #, defaultFile=defFile)
        succ = fdialog.ShowModal()
        if (succ == wx.ID_OK):
            fpath = fdialog.GetPath()

            fid = open(fpath, 'w', encoding='utf8')
            json.dump(results, fid, indent=4, sort_keys=True)
            fid.close()

        return results
예제 #9
0
    def OnMapAstigmaticZ(self, event=None, useMD = True):
        """

        Uses sigmax and sigmay values from astigmatic fits to look up a z-position using calibration curves.
        See Analysis.points.astigmatism.astigTools.lookup_astig_z

        Parameters
        ----------

            None


        Notes
        -----

        """

        from PYME.recipes.multiview import MapAstigZ
        #from PYME.IO import unifiedIO
        pipeline = self.pipeline

        # FIXME - Rename metadata key to be more reasonable
        stigLoc = pipeline.mdh.getOrDefault('Analysis.AstigmatismMapID', None)

        if (not stigLoc is None) and useMD:
            #s = unifiedIO.read(stigLoc)
            #astig_calibrations = json.loads(s)
            pathToMap = stigLoc
        else:
            fdialog = wx.FileDialog(None, 'Load Astigmatism Calibration', wildcard='Astigmatism map (*.am)|*.am',
                                    style=wx.FD_OPEN, defaultDir=nameUtils.genShiftFieldDirectoryPath())
            succ = fdialog.ShowModal()
            if (succ == wx.ID_OK):
                fpath = fdialog.GetPath()

                fdialog.Destroy()
                # load json
                #with open(fpath, 'r') as fid:
                #    astig_calibrations = json.load(fid)
                pathToMap = fpath
            else:
                fdialog.Destroy()
                logger.info('User canceled astigmatic calibration selection')
                return

        recipe = self.pipeline.recipe
        
        # # hold off auto-running the recipe until we configure things
        # recipe.trait_set(execute_on_invalidation=False)
        # try:
        #     mapping_module = MapAstigZ(recipe, input_name=self.pipeline.selectedDataSourceKey,
        #                                astigmatism_calibration_location=pathToMap, output_name='z_mapped')
        #
        #     recipe.add_module(mapping_module)
        #     if not recipe.configure_traits(view=recipe.pipeline_view, kind='modal'):
        #         return
        #
        #     # FIXME - figure out why configuring just the new module doesn't give us an OK button
        #     # if not mapping_module.configure_traits(view=mapping_module.pipeline_view):
        #     #     return #handle cancel
        #     # recipe.add_module(mapping_module)
        #
        #     recipe.execute()
        # finally:  # make sure that we configure the pipeline recipe as it was
        #     recipe.trait_set(execute_on_invalidation=True)

        mapping_module = MapAstigZ(recipe, input_name=self.pipeline.selectedDataSourceKey,
                                   astigmatism_calibration_location=pathToMap, output_name='z_mapped')

        if mapping_module.configure_traits(kind='modal'):
            recipe.add_modules_and_execute([mapping_module,])
            
            # keep a reference for debugging
            
            self._amm = mapping_module
            
            self.pipeline.selectDataSource('z_mapped')
            self.visFr.RefreshView() #TODO - is this needed?
예제 #10
0
    def _on_fit(self, event=None):
        from PYME.recipes.base import ModuleCollection
        from nep_fitting.recipe_modules import nep_fits
        from PYME.IO.ragged import RaggedCache
        from PYME.IO.FileUtils import nameUtils
        import webbrowser
        from nep_fitting import reports

        rec = ModuleCollection()

        rec.add_module(
            nep_fits.FitProfiles(
                rec,
                inputName='line_profiles',
                fit_type=list(profile_fitters.non_ensemble_fitters.keys())[0],
                outputName='output'))

        # populate namespace with current profiles
        rec.namespace['line_profiles'] = RaggedCache(
            self._line_profile_handler.get_line_profiles())
        if not rec.configure_traits(view=rec.pipeline_view, kind='modal'):
            return  # handle cancel

        res = rec.execute()

        fdialog = wx.FileDialog(
            None,
            'Save results as ...',
            wildcard='hdf (*.hdf)|*.hdf',
            style=wx.FD_SAVE,
            defaultDir=nameUtils.genShiftFieldDirectoryPath(
            ))  # , defaultFile=defFile)
        succ = fdialog.ShowModal()
        if (succ == wx.ID_OK):
            base_path = os.path.splitext(fdialog.GetPath())[0]

            res.to_hdf(
                base_path + '.hdf', tablename='profile_fits'
            )  # table name changed to avoid conflicts with standard fit data

            fitter = rec.modules[
                0].fitter  # TODO - move plot_results out from class so we don't have to hack like this
            profile_dir = base_path + '/'
            os.mkdir(profile_dir)
            fitter.plot_results(profile_dir)

            htmlfn = base_path + '.html'

            context = {
                'results':
                res,
                'filename':
                self._dsviewer.image.filename,
                'fittype':
                res.mdh['FitProfiles.FitType'],
                'img_schematic':
                reports.img_as_strb64(
                    reports.get_schematic(res.mdh['FitProfiles.FitType']))
            }
            handler_names = self._line_profile_handler.get_image_names()
            if (len(handler_names) == 1) and (handler_names[0]
                                              == self.image_name):
                # if there's only a single image, include it in the report
                context['img_data'] = reports.img_as_strb64(
                    self._dsviewer.view.GrabPNGToBuffer())

            reports.generate_and_save(htmlfn,
                                      context,
                                      template_name='single_data.html')

            webbrowser.open('file://' + htmlfn, 2)
예제 #11
0
    def OnCalibrateMultiview(self, event):
        """
        CalibrateMultiview loops over all channels described in the metadata and runs CalibrateAstigmatism on each one.
        Results are stored in a library of dictionaries and are saved into a jason astigmatism map file (.am)
        Args:
            event: GUI event

        Returns:
            nothing

        """
        # first make sure the user is calling the right function
        try:
            chanSizeX = self.image.mdh['Multiview.ROISize'][0]
        except KeyError:
            raise KeyError(
                'You are not looking at multiview data, or the metadata is incomplete'
            )

        if len(self.PSFLocs) > 0:
            print(
                'Place cursor on bead in first multiview channel and press Calibrate Multiview Astigmatism'
            )
            self.OnClearTags(event)
            return

        from PYME.Analysis.PSFEst import extractImages
        from PYME.DSView.modules import psfTools
        import scipy.interpolate as terp
        import numpy as np
        from PYME.IO.FileUtils import nameUtils
        import os
        import json

        zrange = np.nan * np.ones(2)

        rsx, rsy, rsz = [int(s) for s in self.tPSFROI.GetValue().split(',')]
        # astigDat = []
        astigLib = {}
        for ii in range(self.numChan):
            # grab PSFs, currently relying on user to pick psf in first channel
            xmin, xmax = [(self.do.xp - rsx + ii * chanSizeX),
                          (self.do.xp + rsx + ii * chanSizeX + 1)]
            # dz returns offset in units of frames, dx dy are in pixels
            dx, dy, dz = extractImages.getIntCenter(
                self.image.data[xmin:xmax,
                                (self.do.yp - rsy):(self.do.yp + rsy + 1), :,
                                0])
            self.PSFLocs.append((self.do.xp + dx, self.do.yp + dy, dz))
            self.view.psfROIs = self.PSFLocs
            self.view.Refresh()

            psfROISize = [int(s) for s in self.tPSFROI.GetValue().split(',')]
            psfBlur = [float(s) for s in self.tPSFBlur.GetValue().split(',')]

            psf = extractImages.getPSF3D(self.image.data[:, :, :, 0],
                                         [self.PSFLocs[ii]], psfROISize,
                                         psfBlur)

            if self.cbBackgroundCorrect.GetValue():
                #widefield image - do special background subtraction
                psf = extractImages.backgroundCorrectPSFWF(psf)

            from PYME.DSView.dsviewer import ImageStack, ViewIm3D

            im = ImageStack(data=psf,
                            mdh=self.image.mdh,
                            titleStub='Extracted PSF, Chan %i' % ii)
            im.defaultExt = '*.psf'  #we want to save as PSF by default
            ViewIm3D(im,
                     mode='psf',
                     parent=wx.GetTopLevelParent(self.dsviewer))

            calibrater = psfTools.PSFTools(self.dsviewer, im)
            astigLib['PSF%i' % ii] = (calibrater.OnCalibrateAstigmatism(
                event, plotIt=False))
            # the next line is only ok if each frame is at a different z position, which it should be
            astigLib['PSF%i' %
                     ii]['z'] += dz * self.image.mdh['voxelsize.z'] * 1.e3

            # -- spline interpolate --
            # find region of dsigma which is monotonic
            dsig = terp.UnivariateSpline(
                astigLib['PSF%i' % ii]['z'],
                astigLib['PSF%i' %
                         ii]['dsigma'])  #, s=1.5*len(astigDat[ii]['z']))

            # mask where the sign is the same as the center
            zvec = np.linspace(astigLib['PSF%i' % ii]['z'].min(),
                               astigLib['PSF%i' % ii]['z'].max(), 1000)
            sgn = np.sign(np.diff(dsig(zvec)))
            halfway = len(sgn) / 2
            notmask = sgn != sgn[halfway]

            # find z range for spline generation
            lowerZ = zvec[np.where(notmask[:halfway])[0].max()]
            upperZ = zvec[(len(sgn) / 2 +
                           np.where(notmask[halfway:])[0].min() - 1)]
            astigLib['PSF%i' % ii]['zrange'] = [lowerZ, upperZ]
            zrange = [
                np.nanmin([lowerZ, zrange[0]]),
                np.nanmax([upperZ, zrange[1]])
            ]

            #lowsubZ , upsubZ = np.absolute(astigDat[ii]['z'] - zvec[lowerZ]), np.absolute(astigDat[ii]['z'] - zvec[upperZ])
            #lowZLoc = np.argmin(lowsubZ)
            #upZLoc = np.argmin(upsubZ)

            #
            #astigLib['sigxTerp%i' % ii] = terp.UnivariateSpline(astigLib['PSF%i' % ii]['z'], astigLib['PSF%i' % ii]['sigmax'],
            #                                                    bbox=[lowerZ, upperZ])
            #astigLib['sigyTerp%i' % ii] = terp.UnivariateSpline(astigLib['PSF%i' % ii]['z'], astigLib['PSF%i' % ii]['sigmay'],
            #                                                    bbox=[lowerZ, upperZ])
            astigLib['PSF%i' % ii]['z'] = astigLib['PSF%i' % ii]['z'].tolist()

        astigLib['zRange'] = np.round(zrange).tolist()
        astigLib['numChan'] = self.numChan

        psfTools.plotAstigCalibration(astigLib)

        # save to json file
        #defFile = os.path.splitext(os.path.split(self.visFr.GetTitle())[-1])[0] + '.am'

        fdialog = wx.FileDialog(
            None,
            'Save Astigmatism Calibration as ...',
            wildcard='AstigMAPism file (*.am)|*.am',
            style=wx.FD_SAVE,
            defaultDir=nameUtils.genShiftFieldDirectoryPath(
            ))  #, defaultFile=defFile)
        succ = fdialog.ShowModal()
        if (succ == wx.ID_OK):
            fpath = fdialog.GetPath()

            fid = open(fpath, 'wb')
            json.dump(astigLib, fid)
            fid.close()