jscatter.sasimagelib — Jscatter 1.3.0.2 documentation

# Source code for jscatter.sasimagelib

    # -*- coding: utf-8 -*-
    # written by Ralf Biehl at the Forschungszentrum Jülich ,
    # Jülich Center for Neutron Science 1 and Institute of Complex Systems 1
    #    Jscatter is a program to read, analyse and plot data
    #    Copyright (C) 2015-2019  Ralf Biehl
    #
    #    This program is free software: you can redistribute it and/or modify
    #    it under the terms of the GNU General Public License as published by
    #    the Free Software Foundation, either version 3 of the License, or
    #    (at your option) any later version.
    #
    #    This program is distributed in the hope that it will be useful,
    #    but WITHOUT ANY WARRANTY; without even the implied warranty of
    #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #    GNU General Public License for more details.
    #
    #    You should have received a copy of the GNU General Public License
    #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    #
    """
    Read 2D image files (TIFF) from SAXS cameras and extract the corresponding data.

    The sasImage is a 2D array that allows direct subtraction and multiplication (e.g. transmission)
    respecting given masks in operations. E.g. ::

     sample=js.sas.sasImage('sample.tiff')
     solvent=js.sas.sasImage('solvent.tiff')
     corrected = sample/sampletransmission - solvent/solventtransmission

    Image manipulation like Gaussian filter from scipy.ndimage can be used.

    Calibration of detector distance including  offset detector positions, radial average, size reduction and more.
     .pickCenter allows sensitive detection of the beamcenter in SAS geometry.
     .calibrateOffsetDetector allows sensitive calibration of the detector position parameters.

    An example is shown in :py:class:`~.sasimagelib.sasImage` .

    ------

    """

    import os
    import glob
    import copy
    import functools
    import pickle
    import numbers
    import warnings
    from collections import deque
    from xml.etree import ElementTree

    import numpy as np
    import numpy.ma as ma
    import scipy
    import scipy.linalg as la
    from scipy import ndimage
    from scipy.interpolate import griddata, LinearNDInterpolator, NearestNDInterpolator
    from scipy.spatial.transform import Rotation

    import PIL
    import PIL.ImageOps
    import PIL.ExifTags
    import PIL.ImageSequence

    import matplotlib.cm as cm
    from matplotlib import colors
    from matplotlib.patches import Circle
    from matplotlib import pyplot
    from matplotlib.widgets import Button

    from . import formel
    from .dataarray import dataArray as dA
    from . import mpl
    from . import structurefactor as sf

    #: normalized gaussian function
    def _gauss(x, A, mean, sigma, bgr):
        return A * np.exp(-0.5 * (x - mean) ** 2 / sigma ** 2) / sigma / np.sqrt(2 * np.pi) + bgr

    def shortprint(values, threshold=6, edgeitems=2):
        """
        Creates a short handy representation string for array values.

        Parameters
        ----------
        values : object
            Values to print.
        threshold: int default 6
            Number of elements to switch to reduced form.
        edgeitems : int default 2
            Items at the edge.

        """
        opt = np.get_printoptions()
        np.set_printoptions(threshold=threshold, edgeitems=edgeitems)
        valuestr = np.array_str(values)
        np.set_printoptions(**opt)
        return valuestr

    def _w2f(word):
        """
        Converts strings if possible to float.
        """
        try:
            return float(word)
        except ValueError:
            return word

    # noinspection PyMissingOrEmptyDocstring
    def parseXML(text):
        root = ElementTree.fromstring(text)
        r = etree_to_dict(root)
        return r

    # noinspection PyMissingOrEmptyDocstring
    def etree_to_dict(root):
        # d = {root.tag : map(etree_to_dict, root.getchildren())}
        d = {child.attrib['name']: child.text for child in root.iter() if child.text is not None}
        return d

    def phase(phases):
        """Transform to [-pi,pi] range."""
        return (phases + np.pi) % (2 * np.pi) - np.pi

    # noinspection PyIncorrectDocstring
    def sImemoize(cachesize=4, attrnames=[]):
        """
        A least-recently-used cache decorator to cache attributes from sasImages dependent on the position attributes.

        Cache is according to attribute names of the used class given as list in attrnames.
        default is empty list

        We use this to avoid multiple calculation of some sasImage parameters for same detector settings
        Therefore a small cachesize is ok.

        """

        def _memoize(function):
            function.hitsmisses = [0, 0]
            cache = function.cache = {}
            deck = function.deck = deque([], maxlen=cachesize)
            function.last = lambda i=-1: function.cache[function.deck[i]]
            clsid = function.clsid = None

            def clear():
                while len(function.deck) > 0:
                    del function.cache[function.deck.pop()]
                function.hitsmisses = [0, 0]

            function.clear = clear

            @functools.wraps(function)
            def _memoizer(*args, **kwargs):
                # This is the class of the memoized method
                cls = args[0]
                # make relevant attribute list and corresponding key
                attributes = [getattr(cls, an, None) for an in attrnames]
                key = pickle.dumps(attributes, protocol=1)
                if function.clsid != id(cls) and len(cache) > 0:
                    # if wrong id we clear the cache not returning wrong stuff
                    # because of a copy includes the cache
                    function.clear()
                if len(cache) == 0:
                    # update clsid at first call
                    function.clsid = id(cls)

                if key in cache:
                    function.hitsmisses[0] += 1
                    deck.remove(key)
                    deck.append(key)
                    return cache[key]
                else:
                    function.hitsmisses[1] += 1
                    cache[key] = function(*args, **kwargs)
                    if len(deck) >= cachesize:
                        del cache[deck.popleft()]
                    deck.append(key)
                    return cache[key]

            return _memoizer

        return _memoize

    # calc peak positions of AgBe
    # q=np.r_[0.5:10:0.0001]
    # iq=js.sas.AgBeReference(q,data.wavelength[0]/10,n=np.r_[1:15])
    # iq.iX[scipy.signal.argrelmax(iq.iY,order=3)[0]]

    #: AgBe peak positions
    AgBepeaks = [1.0753, 2.1521, 3.2286, 4.3049, 5.3813, 6.4576, 7.5339, 8.6102, 9.6865, 10.7628]

    #: Create AgBe peak positions profile
    def _agbpeak(q, center=0, fwhm=1, lg=1, asym=0, amplitude=1, bgr=0):
        peak = formel.voigt(x=q, center=center, fwhm=fwhm, lg=lg, asym=asym, amplitude=amplitude)
        peak.Y += bgr
        return peak

    # While reading the image file, data are extracted from XML string or text in the EXIF data of the image.
    # The following describe what to extract in an line/entry and how to replace:
    # 1 name to look for
    # 2 the new attribute name (to have later unique names from different detectors)
    # 3 a dictionary of char to replace in the line before looking for the keyword/content
    # 4 factor to convert to specific units
    # 5 return value 'list' or 'string', default list with possible conversion to float
    # Not extracted information is in .artist or .imageDescription
    exchangekeywords = [['Wavelength', 'wavelength', None, 1, None],
                        ['Flux', 'flux', None, 1, None],
                        ['det_exposure_time', 'exposure_time', None, 1, None],
                        ['det_pixel_size', 'pixel_size', None, 1, None],
                        ['beamcenter_nominal', 'center', None, 1, None],
                        ['detector_dist', 'detector_distance', None, 0.001, None],
                        ['Meas.Description', 'description', None, 1, 'string'],
                        ['wavelength', 'wavelength', None, 1, None],
                        ['Exposure_time', 'exposure_time', None, 1, None],
                        ['Pixel_size', 'pixel_size', {'m': '', 'x ': ''}, 1, None],
                        ['Detector_distance', 'detector_distance', None, 1, None],
                        ['saxsconf_Izero', 'Izero', None, 1, None],
                        ['sample_transfact', 'transmission_factor', None, 1, None],
                        ['sample_thickness', 'sample_thickness', None, 1, None],
                        ['ygon', 'position_y', None, 1, None],
                        ['zgon', 'position_z', None, 1, None]]

    class SubArray(np.ndarray):
        """see __new__"""

        def __new__(cls, arr):
            """
            Subclass used in sasImage.

            Dont use this directly as intended use is through sasImage.

            Defines a generic np.ndarray subclass, that stores some metadata in attributes
            It seems to be the default way for subclassing maskedArrays to have the array_finalize from this subclass.

            """
            x = np.asanyarray(arr).view(cls)
            x.comment = []
            return x

        def __array_finalize__(self, obj):
            if callable(getattr(super(SubArray, self), '__array_finalize__', None)):
                super(SubArray, self).__array_finalize__(obj)
            if hasattr(obj, 'attr'):
                for attribut in obj.attr:
                    self.__dict__[attribut] = getattr(obj, attribut)
            try:
                # copy tags from reading
                self._tags = getattr(obj, '_tags')
            except AttributeError:
                pass
            return

        @property
        def array(self):
            """As bare array"""
            return self.view(np.ndarray)

        def setattr(self, objekt, prepend='', keyadd='_'):
            """
            Set (copy) attributes from objekt.

            Parameters
            ----------
            objekt : objekt with attr or dictionary
                Can be a dictionary of names:value pairs like {'name':[1,2,3,7,9]}
                If object has property attr the returned attributenames are copied.
            prepend : string, default ''
                Prepend this string to all attribute names.
            keyadd : char, default='_'
                If reserved attributes (T, mean, ..) are found the name is 'T'+keyadd

            """
            if hasattr(objekt, 'attr'):
                for attribut in objekt.attr:
                    try:
                        setattr(self, prepend + attribut, getattr(objekt, attribut))
                    except AttributeError:
                        self.comment.append('mapped ' + attribut + ' to ' + attribut + keyadd)
                        setattr(self, prepend + attribut + keyadd, getattr(objekt, attribut))
            elif isinstance(objekt, dict):
                for key in objekt:
                    try:
                        setattr(self, prepend + key, objekt[key])
                    except AttributeError:
                        self.comment.append('mapped ' + key + ' to ' + key + keyadd)
                        setattr(self, prepend + key + keyadd, objekt[key])

        @property
        def attr(self):
            """
            Show specific attribute names as sorted list of attribute names.

            """
            if hasattr(self, '__dict__'):
                return sorted([key for key in self.__dict__ if key[0] != '_'])
            else:
                return []

        def showattr(self, maxlength=None, exclude=None):
            """
            Show specific attributes with values as overview.

            Parameters
            ----------
            maxlength : int
                Truncate string representation after maxlength char.
            exclude : list of str,default=['comment']
                List of attribute names to exclude from result.

            """
            if exclude is None:
                exclude = ['comment']
            for attr in self.attr:
                if attr not in exclude:
                    values = getattr(self, attr)
                    # noinspection PyBroadException
                    try:
                        valstr = shortprint(values.split('\n'))
                        print('{:>24} = {:}'.format(attr, valstr[0]))
                        for vstr in valstr[1:]:
                            print('{:>25}  {:}'.format('', vstr))
                    except:
                        print('%24s = %s' % (attr, str(values)[:maxlength]))

        def __repr__(self):
            # hide that we have a ndarray subclass, just not to confuse people
            return self.view(np.ndarray).__repr__()

    subarray = SubArray

    # noinspection PyProtectedMember,PyMissingOrEmptyDocstring
    class PickerBeamCenter:
        """ see init """

        def __init__(self, circle, image, destination, symmetry=6):
            """
            Class to pick the center of calibration sasImage

            circle :
                Circle around center to move
            image :
                image to use for calculation of profiles
            destination :
                axes where to plot profiles
            symmetry :
                number of sectors to use for averaging

            """
            self.circle = circle
            self.fig = circle.figure
            self.ax = circle.figure.axes[0]
            self.image = image
            self.imagegauss = ndimage.filters.gaussian_filter(image.data, 0.8)
            self.iX = image.iX
            self.iY = image.iY
            self.symmetry = symmetry
            self.destination = destination
            self.cidpress = circle.figure.canvas.mpl_connect('button_press_event', self.on_button_press)
            self.cidscroll = circle.figure.canvas.mpl_connect('scroll_event', self.on_scroll)
            self.keypress = circle.figure.canvas.mpl_connect('key_press_event', self.on_keypress)
            self.destination.text(circle.radius, 0.95,
                                  'center \n[{0:.1f}, {1:.1f}]'.format(self.circle.center[1], self.circle.center[0]),
                                  fontsize=8)
            self.radialwidth = circle.radius * 0.3
            self.update()

        def on_button_press(self, event):
            if event.inaxes is None:
                return
            if event.button > 1:
                newradius2 = (event.xdata - self.circle.center[0]) ** 2 + (event.ydata - self.circle.center[1]) ** 2
                self.circle.set_radius(newradius2 ** 0.5)
            else:
                self.circle.center = event.xdata, event.ydata
            self.update()

        def on_scroll(self, event):
            if event.inaxes is None:
                return
            if event.button == 'down':
                self.circle.set_radius(self.circle.radius - 1)
            elif event.button == 'up':
                self.circle.set_radius(self.circle.radius + 1)
            self.update()

        def on_keypress(self, event):
            pressedkey = event.key
            # print('-',str(pressedkey), '-')
            if pressedkey == 'up':
                self.circle.center = self.circle.center[0], self.circle.center[1] - 1
            elif pressedkey == 'down':
                self.circle.center = self.circle.center[0], self.circle.center[1] + 1
            elif pressedkey == 'left':
                self.circle.center = self.circle.center[0] - 1, self.circle.center[1]
            elif pressedkey == 'right':
                self.circle.center = self.circle.center[0] + 1, self.circle.center[1]
            elif pressedkey == 'ctrl+up':
                self.circle.center = self.circle.center[0], self.circle.center[1] - 0.1
            elif pressedkey == 'ctrl+down':
                self.circle.center = self.circle.center[0], self.circle.center[1] + 0.1
            elif pressedkey == 'ctrl+left':
                self.circle.center = self.circle.center[0] - 0.1, self.circle.center[1]
            elif pressedkey == 'ctrl+right':
                self.circle.center = self.circle.center[0] + 0.1, self.circle.center[1]
            elif pressedkey == 'u':
                pass
            elif pressedkey == '+':
                self.circle.set_radius(self.circle.radius + 1)
            elif pressedkey == '-':
                self.circle.set_radius(self.circle.radius - 1)
            elif pressedkey == 'ctrl++':
                self.radialwidth += 1
            elif pressedkey == 'ctrl+-':
                self.radialwidth -= 1
            self.update()

        def update(self):
            dphi = 2 * np.pi / self.symmetry
            # calc azimuth and radial with new center
            self.image.setPlaneCenter([self.circle.center[1], self.circle.center[0]])
            azimuth = self.image._polarazimuth
            radial = self.image._polarradial
            awidth = dphi / 2
            image = self.imagegauss
            for i, angle in enumerate(np.r_[-np.pi:np.pi:dphi]):
                mask = ((azimuth > (angle - awidth)) & (azimuth < (angle + awidth)) &
                        (radial > self.circle.radius - self.radialwidth) & (radial < self.circle.radius + self.radialwidth))
                # noinspection PyBroadException
                try:
                    rad = dA(np.stack([radial[mask], image[mask]]))
                    rad.isort()  # sorts along X by default
                    # return lower number of points from prune
                    result = rad[:, rad.Y > 0].prune(number=50, type='sum', kind='lin')
                    result.Y = result.Y / result.Y.max()
                    if len(self.destination.lines) > i:
                        # update data
                        self.destination.lines[i].set_xdata(result.X)
                        self.destination.lines[i].set_ydata(result.Y)
                    else:
                        # line not yet plotted
                        self.destination.plot(result.X, result.Y)
                except:
                    pass
            self.destination.set_xlim(self.circle.radius * 0.7, self.circle.radius * 1.3)
            self.destination.texts[0].set_text(
                'center \n[{0:.1f}, {1:.1f}]'.format(self.circle.center[1], self.circle.center[0]))
            self.destination.texts[0].set_position([self.circle.radius, 0.95])
            self.fig.canvas.draw_idle()

    # noinspection PyProtectedMember,PyMissingOrEmptyDocstring
    class PickerDetPosition:
        """ see init """

        def __init__(self, image, lattice, axes, buttons, latticeparameters):
            """
            Class to pick the detector position of calibration sasImage

            We have 2 axes with the image and the lines to align.
            We use buttons to update the image parameters directly and use a single update method for all.

            """
            self.lattice = lattice
            self.domainsize = latticeparameters[0]
            self.asym = latticeparameters[1]
            self.lg = latticeparameters[2]
            self.rmsd = latticeparameters[3]
            self.hklmax = latticeparameters[4]
            self.image = image
            self.iX = image.iX
            self.iY = image.iY
            self.axdif = axes[0]
            self.axline = axes[1]
            self.figure = axes[0].figure
            self.dsteps = [0.0001, 0.001, 0.01, 0.1]
            self.dstepi = 2
            self.asteps = [0.1, 3, 30]
            self.astepi = 1
            self.csteps = [0.1, 1, 10, 100]
            self.cstepi = 1
            self.buttons = buttons
            self.buttons[0].on_clicked(self.bstep)
            self.buttons[1].on_clicked(self.bcenterx_p)
            self.buttons[2].on_clicked(self.bcenterx_m)
            self.buttons[3].on_clicked(self.bcentery_p)
            self.buttons[4].on_clicked(self.bcentery_m)
            self.buttons[5].on_clicked(self.bdistance_p)
            self.buttons[6].on_clicked(self.bdistance_m)
            self.buttons[7].on_clicked(self.balpha_p)
            self.buttons[8].on_clicked(self.balpha_m)
            self.buttons[9].on_clicked(self.bbeta_p)
            self.buttons[10].on_clicked(self.bbeta_m)
            self.buttons[11].on_clicked(self.bgamma_p)
            self.buttons[12].on_clicked(self.bgamma_m)
            self.buttons[13].on_clicked(self.bastep)
            self.buttons[14].on_clicked(self.bcstep)

            self.axline.text(0.7, 0.6, 'center \n[{0:.1f}, {1:.1f}]'.format(self.image.center[0], self.image.center[1]),
                             fontsize=8)

            self.vmax = self.axline.lines[1].get_ydata().max()
            self.vmin = self.vmax * 0.1
            self.cidpress = axes[1].figure.canvas.mpl_connect('button_press_event', self.on_button_press)
            self.update()

        def bstep(self, event):
            # distance steps
            self.dstepi = (self.dstepi + 1)
            if self.dstepi >= len(self.dsteps):
                self.dstepi = 0
            self.buttons[0].label.set_text(r'step $\pm${0:}'.format(self.dsteps[self.dstepi] * 1000))
            self.figure.canvas.draw_idle()

        def bastep(self, event):
            # angular step
            self.astepi = (self.astepi + 1)
            if self.astepi >= len(self.asteps):
                self.astepi = 0
            self.buttons[13].label.set_text(r'step $\pm${0:}'.format(self.asteps[self.astepi]))
            self.figure.canvas.draw_idle()

        def bcstep(self, event):
            # center step
            self.cstepi = (self.cstepi + 1)
            if self.cstepi >= len(self.csteps):
                self.cstepi = 0
            self.buttons[14].label.set_text(r'step $\pm${0:}'.format(self.csteps[self.cstepi]))
            self.figure.canvas.draw_idle()

        def bcenterx_p(self, event):
            self.image.setPlaneCenter([self.image.center[0] + self.csteps[self.cstepi], self.image.center[1]])
            self.update()

        def bcenterx_m(self, event):
            self.image.setPlaneCenter([self.image.center[0] - self.csteps[self.cstepi], self.image.center[1]])
            self.update()

        def bcentery_p(self, event):
            self.image.setPlaneCenter([self.image.center[0], self.image.center[1] + self.csteps[self.cstepi]])
            self.update()

        def bcentery_m(self, event):
            self.image.setPlaneCenter([self.image.center[0], self.image.center[1] - self.csteps[self.cstepi]])
            self.update()

        def bdistance_p(self, event):
            self.image.setDetectorDistance(self.image.detector_distance[0] + self.dsteps[self.dstepi])
            self.update()

        def bdistance_m(self, event):
            self.image.setDetectorDistance(self.image.detector_distance[0] - self.dsteps[self.dstepi])
            self.update()

        def balpha_p(self, event):
            self.image.setPlaneOrientation(alpha=self.image.alpha + self.asteps[self.astepi])
            self.update()

        def balpha_m(self, event):
            self.image.setPlaneOrientation(alpha=self.image.alpha - self.asteps[self.astepi])
            self.update()

        def bbeta_p(self, event):
            self.image.setPlaneOrientation(beta=self.image.beta + self.asteps[self.astepi])
            self.update()

        def bbeta_m(self, event):
            self.image.setPlaneOrientation(beta=self.image.beta - self.asteps[self.astepi])
            self.update()

        def bgamma_p(self, event):
            self.image.setPlaneOrientation(gamma=self.image.gamma + self.asteps[self.astepi])
            self.update()

        def bgamma_m(self, event):
            self.image.setPlaneOrientation(gamma=self.image.gamma - self.asteps[self.astepi])
            self.update()

        def on_button_press(self, event):
            if event.inaxes != self.axline:
                return
            if event.button == 1:
                self.vmin = event.ydata
                self.vmax = max(self.vmax, self.vmin * 1.1)
            elif event.button == 3:
                self.vmax = event.ydata
                self.vmin = min(self.vmin, self.vmax * 0.9)
            self.update()

        def update(self):
            domainsize = self.domainsize
            hklmax = self.hklmax
            rmsd = self.rmsd

            # looking at self
            selfradial = self.image.radialAverage()
            self.axline.lines[0].set_xdata(selfradial.X)
            self.axline.lines[0].set_ydata(selfradial.Y)
            bgr = selfradial.Y.min()
            srYmax = selfradial.Y.max()

            # looking at the lattice
            Iradial = sf.radial3DLSF(self.image.pQ.reshape(-1, 3), lattice=self.lattice, domainsize=domainsize,
                                     rmsd=rmsd, hklmax=hklmax, wavelength=self.image.wavelength[0] / 10)
            I2d = Iradial.Y.reshape(self.image.shape)
            # mask values like center extrema not to be included in min/max
            I2d[self.image.mask] = Iradial.Y.min()
            # scale to selfradial
            scaling = (srYmax - bgr) / (Iradial.Y.max() - Iradial.Y.min())
            I2d = (I2d - Iradial.Y.min()) * scaling + bgr
            # it is better to recalc for q values and scale this
            latticeradial = sf.latticeStructureFactor(selfradial.X, lattice=self.lattice, domainsize=domainsize,
                                                      rmsd=rmsd, hklmax=hklmax, wavelength=self.image.wavelength[0] / 10)
            scaling = (srYmax - bgr) / (latticeradial.Y.max() - latticeradial.Y.min())
            self.axline.lines[1].set_xdata(latticeradial.X)
            self.axline.lines[1].set_ydata(latticeradial.Y * scaling + bgr)

            visible = ((min(selfradial.X) < np.r_[Iradial.q_hkl]) & (np.r_[Iradial.q_hkl] < max(selfradial.X)))
            print('[hkl],  2theta :')
            for a, b in zip(Iradial.hkl[visible][:len(Iradial.Braggtheta)],
                            Iradial.Braggtheta[visible[:len(Iradial.Braggtheta)]]):
                print('{0}, {1:.2f}'.format(a, b))

            # set new data and use limits to show signal border lines (clip was set to transparency for outliers)
            self.axdif.images[1].set_data(I2d)
            vmin, vmax = sorted([self.vmin, self.vmax, bgr * 1.1, srYmax * 0.9])[1:3]
            self.axdif.images[1].set_clim(vmin=vmin, vmax=vmax)
            self.axline.lines[2].set_xdata(latticeradial.X)
            self.axline.lines[2].set_ydata(np.ones_like(latticeradial.X) * vmin)
            self.axline.lines[3].set_xdata(latticeradial.X)
            self.axline.lines[3].set_ydata(np.ones_like(latticeradial.X) * vmax)

            self.axline.relim()
            self.axline.autoscale_view()
            self.figure.texts[0].set_text('center    [{0:.1f}, {1:.1f}] pixel\ndistance  {2:.1f}                 mm\n '
                                          r'$\alpha,\beta,\gamma$     '
                                          '{3:.1f}, {4:.1f}, {5:.1f} deg.'.format(
                self.image.center[0], self.image.center[1], self.image.detector_distance[0] * 1000,
                self.image.alpha, self.image.beta, self.image.gamma))

            self.figure.canvas.draw_idle()

    # noinspection PyAbstractClass
    class sasImage(SubArray, np.ma.MaskedArray):
        """ see __new__  """

        def __new__(cls, file, detector_distance=None, center=None,
                    alpha=None, beta=None, gamma=None, pixel_size=None, wavelength=None,
                    copy=None, maskbelow=0):
            r"""
            Creates/reads sasImage as maskedArray from a detector image or array for evaluation.

            - All methods of maskedArrays including masking of invalid areas work.
            - Masked areas are automatically masked for all math operations.
            - Arithmetic operations for sasImages work as for numpy arrays
              e.g. to subtract background image or multiplying with transmission or corrections [1]_.
              Use the numpy.ma methods.
            - Pixel coordinates in images are [height,width] with origin located at upper-left corner.

            Parameters
            ----------
            file : string, PIL.Image, ndarray
                Filename to read as image or created PIL.Image/ndarray to process.
                 - Images are read and information in the EXIF tag is used if present
                   (if not present add manually detector_distance, center,) using the sasImage methods.
                 - numpy arrays with ndim=2 can be used directly
                 - An PIL.Image can be read with .open or created directly from an numpy array
                   (maybe read with np.loadtxt) ::

                    import PIL
                    import numpy as np
                    image0 = PIL.Image.open('filename')        # read image
                    imagearray = np.random.rand(256,256)*100   # example as a random array
                    image1 = PIL.Image.fromarray(imagearray)   # create image from array (try image1.show())
                    # create sasImage with needed parameters
                    si=js.sas.sasImage(image1,1,[100,100],pixel_size=0.001,wavelength=2)

            detector_distance : float, sasImage, optional
                Detector distance from calibration measurement or calibrated image in unit m.
                Overwrites value in the file EXIF tag.
            center : None, list 2xfloat, sasImage, optional
                Center is [height, width] pixel position of closest point to sample or primary beam in standard SAS geometry
                with origin located at upper-left corner.
                Overwrites value given in the file EXIF tag.
            alpha : float, optional
                Rotation angle between incident beam and detector plane normal in degree.
            beta : float, optional
                Rotation angle of Detector pixel X dimension around detector plane normal in degree.
            gamma : float, optional
                Rotation of detector normal around incident beam in degree.
            pixel_size : [float,float], optional
                Pixel size in [x,y] direction in units m.
            wavelength : float, optional
                Wavelength in units angstrom.
            copy : sasImage, optional
                Copy center, detector_distance, alpha, beta, gamma, wavelength, pixel_size from sasImage.
                Overwrites corresponding data from file.
            maskbelow : float, default =0, optional
                Mask values below this value.

            Returns
            -------
                image : sasImage with attributes
                 - .center : center
                 - .iX : Height pixel positions
                 - .iY : Width pixel positions
                 - .filename
                 - .artist : Additional attributes from EXIF Tag Artist
                 - .imageDescription : Additional attributes from EXIF Tag ImageDescription
                 - .detector_distance
                 - .alpha, .beta, .gamma as detector plane orientation
                 - .wavelength wavelength in units Å.
                 - .pixel_size in units m.

            Notes
            -----
            - Unmasked data can be accessed as .data
            - The mask is .mask and initial set to all negative values.
            - Masking of a pixel is done as ``image[i,j]=np.ma.masked``.
              Use mask methods as implemented.
            - Geometry mask methods can be used and additional masking methods from numpy masked Arrays.
              ::

               import jscatter as js
               from numpy import ma
               cal = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
               cal.mask = ma.nomask                  # reset mask
               cal[cal<0]= ma.masked                 # mask negative values
               cal[(cal>30) & (cal<100)] = ma.masked # mask region of values

            - TIFF tags with index above 700 are ignored.

            - Tested for reading tiff image files from Pilatus detectors as given from our
              metal jet SAXS machines Ganesha and Galaxi at JCNS, Jülich.
            - Additional SAXSpace TIFF files are supported which show frames per pixel on the Y axis.
              This allows to examine the time evolution of the measurement on these line collimation cameras
              (Kratky camera).
              Instead of the old PIL the newer fork Pillow is needed for the multi page TIFFs.
              Additional the pixel_size is set to 0.024 (µm) as for the JCNS CCD camera.
            - Center & orientation:

              The x,y orientation for images are not well defined and dependent
              on the implementation on the specific camera setup.
              Typically coordinates are used in  [height,width] with the origin in the upper left corner.
              This is opposed to the expectation of [x,y] coordinates with the X horizontal
              and the origin at the lower-left.
              To depict 2D images in the way we expect it from the experimental setup
              (location of the center, orientation) it is not useful to change orientation.
              Correspondingly the first coordinate (usually expected X) is the height coordinate in vertical direction.
            - For convenient reading of several images:

              1 Read calibration measurement as ::

                 cal=js.sas.sasImage('mycalibration.tif')

              2 Determine detector distance and center which are stored in calibration sasImage.
              3 Read following sasImages copying the information stored in sasImage ``cal`` by ::

                 sample=js.sas.sasImage('nextsample.tif', copy=cal)

            Examples
            --------
            ::

             import jscatter as js
             #
             # Look at calibration measurement
             calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             # Check center
             # For correct center it should show straight lines (change center to see change)
             calibration.showPolar(center=calibration.center,scaleR=3)
             # or use pickBeamcenter which seems to be more accurate
             calibration.pickBeamcenter()

             # Recalibrate with previous found center (calibration sets it already)
             calibration.recalibrateDetDistance(showfits=True)
             iqcal=calibration.radialAverage()
             # This might be used to calibrate detector distance for following measurements as
             # empty.setDetectorDistance(calibration)
             #
             empty = js.sas.sasImage(js.examples.datapath+'/emptycell.tiff')
             # Mask beamstop (not the same as calibration, unluckily)
             empty.mask4Polygon([370,194],[380,194],[466,0],[456,0])
             empty.maskCircle(empty.center, 17)
             empty.show()
             buffer = js.sas.sasImage(js.examples.datapath+'/buffer.tiff')
             buffer.maskFromImage(empty)
             buffer.show()
             bsa = js.sas.sasImage(js.examples.datapath+'/BSA11mg.tiff')
             bsa.maskFromImage(empty)
             bsa.show() # by default a log scaled image
             #
             # subtract buffer (transmission factor is just a guess here, sorry)
             new=bsa-buffer*0.2
             new.show()
             #
             iqempty=empty.radialAverage()
             iqbuffer=buffer.radialAverage()
             iqbsa=bsa.radialAverage()
             #
             p=js.grace(1,1)
             p.plot(iqempty,le='empty cell')
             p.plot(iqbuffer,le='buffer')
             p.plot(iqbsa,le='bsa 11 mg/ml')
             p.title('raw data, no transmission correction')
             p.yaxis(min=1,max=1e3,scale='l',label='I(q) / a.u.')
             p.xaxis(scale='l',label='q / nm\S-1')
             p.legend()

            References
            ----------

            .. [1] Everything SAXS: small-angle scattering pattern collection and correction
                   Brian Richard Pauw J. Phys.: Condens. Matter 25,  383201 (2013)
                   DOI https://doi.org/10.1088/0953-8984/25/38/383201

            """
            # open file
            if isinstance(file, str):
                # read tiff image
                image = PIL.Image.open(file)
            elif isinstance(file, np.ndarray) and np.ndim(file) == 2:
                image = PIL.Image.fromarray(file)
            else:
                # try if this was an opened image
                image = file

            # get EXIF tags
            if hasattr(image, 'tag_v2'):
               tags = image.tag_v2
            elif hasattr(image,'tag'):
                tags = image.tag
            else:
                tags={}

            # convert image to a writeable array (np.array creates by default a copy)
            # this ensures that we can write to the array
            try:
                # try im we have multiple frames as for SAXSpace
                # seek(1) returns error for single frame
                image.seek(1)
                image.seek(0)
                # squeeze for single columns
                im = np.array([np.array(image) for _ii in PIL.ImageSequence.Iterator(image)]).squeeze()

            except (EOFError, AttributeError):
                # tif to array conversion for single frame
                # im  = np.asarray(image.transpose(PIL.Image.FLIP_TOP_BOTTOM))
                im = np.array(image)

            # create the maskedArray from the base class as view
            # create default mask from values smaller zero
            # Pilatus detectors have negative values outside sensitive detector area.
            sub_im = SubArray(im)
            data = np.ma.MaskedArray.__new__(cls, data=sub_im, mask=sub_im < maskbelow)

            # default values
            data.imageDescription = []
            data.artist = []
            data.set_fill_value(0)
            # the EXIF tags contain all meta information.
            # Take them as dictionary and add to artist, imageDescription or respective name from PIL.ExifTags.TAGS.
            data._getEXIF(tags)
            # set attributes from exif and extract some of these data
            data.filename = file
            data.description = '---'
            # keywords to replace
            data._extractAttributes_(exchangekeywords)
            if copy is not None:
                data.setAttrFromImage(copy)
            else:
                if center is not None:
                    data.setPlaneCenter(center)
                if detector_distance is not None:
                    data.setDetectorDistance(detector_distance)
                if pixel_size is not None:
                    data.setPixelSize(pixel_size)
                if wavelength is not None:
                    data.setWavelength(wavelength)
                data.setPlaneOrientation(0 if alpha is None else alpha,
                                         0 if beta is None else beta,
                                         0 if gamma is None else gamma)

            data._issasImage = True

            return data

        def _extractAttributes_(self, attriblist):
            # extract attributes from EXIF entries
            # first words in comments
            firstwords = [line.split()[0] for line in self.imageDescription + self.artist if len(line.strip()) > 0]
            for attribs in attriblist:
                if attribs[0] in firstwords:
                    self.getfromcomment(attribs[0], replace=attribs[2], newname=attribs[1])
                    if attribs[4] == 'string':
                        setattr(self, attribs[1], ' '.join([str(v) for v in getattr(self, attribs[1])]))
                    else:
                        setattr(self, attribs[1],
                                [v * attribs[3] if isinstance(v, numbers.Number) else v for v in getattr(self, attribs[1])])

        # noinspection PyBroadException
        def _getEXIF(self, tags):
            # Take them as dictionary and add to artist, imageDescription or respective name from PIL.ExifTags.TAGS.
            self._tags = tags
            # extract EXIF data and save them in artist and imageDescription
            for k, v in dict(self._tags).items():
                if k > 700:
                    continue
                elif k == 270:
                    # TAGS[270] = 'ImageDescription'
                    # from Galaxy or Ganesha
                    self.setattr(
                        {'imageDescription': [vv[1:].strip() if vv[0] == '#' else vv.strip() for vv in v.splitlines()]})
                elif k == 315:
                    # TAGS[315] =  'Artist'
                    # in XML tag from Ganesha. Throws error if not a XML tag as for Galaxy
                    try:
                        self.entriesXML = parseXML(self._tags[315])
                        self.setattr({'artist': [str(k) + ' ' + str(v) for k, v in self.entriesXML.items()]})
                    except ElementTree.ParseError:
                        if isinstance(self._tags[315], str):
                            # catch if it is a single string as for SAXSPACE
                            self.setattr({'artist': [self._tags[315]]})
                        else:
                            self.setattr({'artist': []})
                else:
                    if k in PIL.ExifTags.TAGS:
                        self.setattr({PIL.ExifTags.TAGS[k]: v if isinstance(v, (list, set)) else [v]})
            try:
                if self.artist[0] == 'Anton Paar GmbH':
                    # catches SAXSPACE TIFF files
                    # iv are specific for SAXSPACE
                    for k, iv in dict({'wavelength': 65024, 'detector_distance': 65060}).items():
                        v = self._tags[iv]
                        self.setattr({k: v if isinstance(v, (list, set)) else [v]})
                    self.pixelSize = 0.024  # 24 µm
            except:
                pass

            return

        def _setEXIF(self):
            # set Exif entries according to attributes if these were changed
            # see PIL.TiffTags.TYPES for types
            # we add anything new to TAGS[270]
            for k, v in dict(self._tags).items():
                if k > 700:
                    continue
                elif k == 270:
                    # TAGS[270] = 'ImageDescription'
                    content = ['processed by Jscatter']
                    content += self.imageDescription
                    for ekw in exchangekeywords:
                        # noinspection PyBroadException
                        try:
                            content.append(ekw[0] + ' ' + ' '.join([str(a) for a in getattr(self, ekw[1])]))
                        except:
                            pass
                    self._tags[k] = '\n'.join(content)
                elif k == 315:
                    # TAGS[315] = 'Artist'
                    self._tags[k] = '\n'.join(self.artist)
                else:
                    if k in PIL.ExifTags.TAGS:
                        content = getattr(self, PIL.ExifTags.TAGS[k])[0]
                        typ = self._tags.tagtype[k]
                        if typ == 2:
                            self._tags[k] = ' '.join(content)
                        elif typ in [3, 4, 8, 9]:
                            self._tags[k] = content
                        else:
                            self._tags[k] = content
            return

        @property
        @sImemoize(cachesize=1, attrnames=[])
        def iY(self):
            """
            Y pixel coordinates

            """
            return np.repeat(np.r_[0:self.shape[1]][None, :], self.shape[0], axis=0)

        @property
        @sImemoize(cachesize=1, attrnames=[])
        def iX(self):
            """
            X pixel coordinates

            """
            return np.repeat(np.r_[0:self.shape[0]][:, None], self.shape[1], axis=1)

        @property
        def array(self):
            """
            Strip of all attributes and return a simple array without mask.
            """
            return self.data.array

        def __repr__(self):
            center = self.center if hasattr(self, 'center') else None
            detector_distance = self.detector_distance if hasattr(self, 'detector_distance') else None
            alpha = self.alpha if hasattr(self, 'alpha') else None
            beta = self.beta if hasattr(self, 'beta') else None
            gamma = self.gamma if hasattr(self, 'gamma') else None

            desc = "sasImage-> \n{0} \ncenter={1} \ndetector distance={2} \nalpha, beta, " \
                   "gamma ={3},{4},{5} \nshape={6} "
            return desc.format(self, center, detector_distance, alpha, beta, gamma, self.shape)

        def getfromcomment(self, name, replace=None, newname=None):
            """
            Extract name from .artist or .imageDescription with attribute name in front.

            If multiple names start with parname first one is used.
            Used line is deleted from .artist or .imageDescription.

            Parameters
            ----------
            name : string
                Name of the parameter in first place.
            replace : dict
                Dictionary with pairs to replace in all lines.
            newname : string
                New attribute name

            """
            if newname is None:
                newname = name
            # first look in imageDescription
            for i, line in enumerate(self.imageDescription):
                if isinstance(replace, dict):
                    for k, v in replace.items():
                        line = line.replace(k, str(v))
                words = line.split()
                if len(words) > 0 and words[0] == name:
                    setattr(self, newname, [_w2f(word) for word in words[1:]])
                    del self.imageDescription[i]
                    return
            # then in artist
            for i, line in enumerate(self.artist):
                if isinstance(replace, dict):
                    for k, v in replace.items():
                        line = line.replace(k, str(v))
                words = line.split()
                if len(words) > 0 and words[0] == name:
                    setattr(self, newname, [_w2f(word) for word in words[1:]])
                    del self.artist[i]
                    return

        def setDetectorDistance(self, detector_distance):
            """
            Set detector distance as shortest distance to sample along plane normal vector.

            Parameters
            ----------
            detector_distance : float, sasImage
                New value for detector distance.
                If sasImage the detector_distance is copied.

            Notes
            -----
            EXIF data show this as list so we stay to this.

            """
            if isinstance(detector_distance, numbers.Number):
                self.detector_distance = [detector_distance]
            elif isinstance(detector_distance, (list, set)):
                self.detector_distance = [v if isinstance(v, numbers.Number) else v for v in detector_distance]
            elif isinstance(detector_distance, sasImage):
                self.detector_distance = [v if isinstance(v, numbers.Number) else v
                                          for v in detector_distance.detector_distance]

        def setPlaneCenter(self, center):
            """
            Set center of plane where plane normal has shortest distance to sample.

            In standard SAS geometry this is equal to the beamcenter

            Parameters
            ----------
            center : 2x float, sasImage
                New value for center as [height, width] coordinates.
                If sasImage the center is copied.

            """
            if isinstance(center, sasImage):
                # copy from object
                self.center = list(center.center)
            else:
                self.center = list(center)

        def setPixelSize(self, pixel_size):
            """
            Set pixel_size.

            Parameters
            ----------
            pixel_size : [float,float]
                Pixel size in [x,y] direction in units m.

            """
            if isinstance(pixel_size, sasImage):
                # copy from object
                self.pixel_size = list(pixel_size.pixel_size)
            else:
                if isinstance(pixel_size, numbers.Number):
                    self.pixel_size = [pixel_size]*2
                else:
                    self.pixel_size = list(pixel_size)

        def setWavelength(self, wavelength):
            """
            Set wavelength.

            Parameters
            ----------
            wavelength : [float]
                Wavelength in units angstrom.

            """
            if isinstance(wavelength, sasImage):
                # copy from object
                self.wavelength = list(wavelength.wavelength)
            else:
                if isinstance(wavelength, numbers.Number):
                    self.wavelength = [wavelength]
                else:
                    self.wavelength = list(wavelength[0])

        def setPlaneOrientation(self, alpha=None, beta=None, gamma=None):
            """
            Set orientation angles of detector plane .

            In standard SAS geometry these are equal 0.

            Parameters
            ----------
            alpha : float
                Rotation angle between incident beam and detector plane normal in degree.
            beta : float
                Rotation angle of Detector pixel X dimension around detector plane normal in degree.
            gamma : float
                Rotation of detector normal around incident beam in degree.

            """
            if isinstance(alpha, sasImage):
                # copy from object
                self.alpha = alpha.alpha
                self.beta = alpha.beta
                self.gamma = alpha.gamma
            else:
                if alpha is not None:
                    self.alpha = alpha
                if beta is not None:
                    self.beta = beta
                if gamma is not None:
                    self.gamma = gamma

        def setDetectorPosition(self, center, detector_distance, alpha=None, beta=None, gamma=None):
            """
            Set parameters describing the position and orientation of the detector.

            Parameters
            ----------
            center : 2x float
                Center of the detector where the plane normal is going through the sample origin in pixel units.
                For conventional small angle scattering geometry (detector plane perpendicular to incoming beam)
                this is the beam center.
            detector_distance : float
                Distance of the detector center to sample origin in units m.
            alpha : float
                Rotation angle between incident beam and detector plane normal in degree.
            beta : float
                Rotation angle of Detector pixel X dimension around detector plane normal in degree.
            gamma : float
                Rotation of detector normal around incident beam in degree.

            """
            self.setPlaneCenter(center)
            self.setDetectorDistance(detector_distance)
            self.setPlaneOrientation(alpha, beta, gamma)

        def setAttrFromImage(self, image):
            """
            Copy center, detector_distance, alpha, beta, gamma wavelength, pixel_size from image.

            Parameters
            ----------
            image  sasImage
                sasImage to copy attributes to self.

            """
            self.setPlaneCenter(image)
            self.setDetectorDistance(image)
            self.pixel_size = copy.copy(image.pixel_size)
            self.wavelength = copy.copy(image.wavelength)
            self.alpha = copy.copy(image.alpha)
            self.beta = copy.copy(image.beta)
            self.gamma = copy.copy(image.gamma)

        def pickBeamcenter(self, levels=8, symmetry=6):
            """
            Open image to pick the center from a calibration sample as AgBe in standard SAS geometry.

            Radial averaged sectors allow to find the optimal center with best overlap of peaks.
            Closing the image accepts the actual selected center.
            Standard SAS geometry => alpha=beta=gamma=0

            Parameters
            ----------
            levels : int
                Number of levels in contour image.
            symmetry : int
                Number of sectors around center for radial averages.

            Returns
            -------
                After closing the selected center is saved in the sasImage.

            Notes
            -----
            **How it works**
            A figure with the AgBe picture (right) and a radial average over sectors is shown
            (left, symmetry defines number of sectors) .

            - Beamcenter: A circle is shown around the center.
              Mouse left click changes the center to mouse pointer position.
            - The center can be moved by arrow keys (+-1) or ctrl+arrow (+-0.1)
            - The default radius corresponds to an AgBe reflex.
              By middle or right click the radius can be set to mouse pointer position.
              Additional the radius of the circle (center of left plot data) can be increased/decreased by +/-.
            - Width around radius (for left plot) can be increased/decrease by ctrl++/ctrl+-.
            - A radial average in sectors is calculated (after some smoothing) and shown in the left axes.
            - The center is OK if the peaks show maximum overlap and symmetry.

             Examples
             --------
             ::

              import jscatter as js
              #
              calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
              # use pickBeamcenter
              calibration.pickBeamcenter()

            """
            if mpl._headless:
                warnings.warn('pickBeamcenter cannot be used in headless mode!')
                return

            colorMap = 'jet'
            origin = 'lower'
            fontsize = 10
            extend = None

            wl = self.wavelength[0] / 10.  # conversion to nm
            dd = self.detector_distance[0]
            # pixel r from q
            pfq = lambda q: dd * np.tan(2 * np.arcsin(np.asarray(q) * wl / 4. / np.pi))
            pixelpeaks = pfq(AgBepeaks) / self.pixel_size[0]
            # guess good AgBe peak
            pixelradius = pixelpeaks[np.abs(pixelpeaks - np.min(self.shape) / 5).argmin()]

            figsize = pyplot.figaspect(0.6)
            fig = pyplot.figure(figsize=figsize)
            ax1 = fig.add_axes([0.4, 0.05, 0.6, 0.85])
            ax0 = fig.add_axes([0.1, 0.1, 0.3, 0.8])
            cmap = pyplot.get_cmap(colorMap)
            lmap = pyplot.get_cmap(None)
            fig.suptitle(
                'Move center: Pick with mouse; Close to accept \narrows(+-1 pixel) or ctrl+arrow (+-0.1 pixel) ',
                fontsize=10)
            ax1.yaxis.tick_right()
            ax1.yaxis.set_label_position("right")
            logself = np.ma.log(self)
            im = ax1.imshow(logself, cmap=cmap, extent=extend, origin=origin)
            im.cset = ax1.contour(logself, levels=levels, linewidths=1, cmap=lmap, extent=extend, origin=origin)
            im.labels = ax1.clabel(im.cset, inline=True, fmt='%1.1f', fontsize=fontsize)
            fig.colorbar(im, ax=ax1, orientation='horizontal', shrink=0.7, fraction=0.03,
                         pad=0.1)  # note that colorbar is a method of the figure, not the axes
            ax1.invert_yaxis()
            ax1.set_xlabel('Y dimension / pixel')
            ax1.set_ylabel('X dimension / pixel')
            ax0.set_xlabel('radius / pixel')
            ax0.set_ylabel('normalized mean count rate / pixel')

            # create circle and add it to figure
            if hasattr(self, 'center'):
                ccenter = [self.center[1], self.center[0], ]
                print('Old position of center: [{0:.2f},{1:.2f}]'.format(ccenter[1], ccenter[0]))
            else:
                ccenter = (self.shape[1] / 2, self.shape[0] / 2)
                print('No center defined')
            circle = Circle(ccenter, pixelradius, color='k', fill=False, linewidth=2, linestyle=(0, (6, 3)))

            ax1.add_artist(circle)
            # create picker and turn matplotlib to blocking mode to wait until window is closed
            pick = PickerBeamCenter(circle=circle, image=self, destination=ax0, symmetry=symmetry)
            mpl.pyplot.show(block=True)
            # now set center
            self.setPlaneCenter([pick.circle.center[1], pick.circle.center[0]])
            print('Set center to [{0:.2f},{1:.2f}]'.format(self.center[0], self.center[1]))

        def maskReset(self):
            """
            Reset the mask.

            By default values smaller 0 are automatically masked again as is also default for reading

            """
            self.mask = ma.nomask
            self[self < 0] = ma.masked

        def maskFromImage(self, image):
            """
            Use/copy mask from image.

            Parameters
            ----------
            image : sasImage
                sasImage to use mask for resetting mask.
                image needs to have same dimension.

            """
            if image.shape == self.shape:
                self.mask = image.mask

        def maskRegion(self, xmin, xmax, ymin, ymax):
            """
            Mask rectangular region.

            Parameters
            ----------
            xmin,xmax,ymin,ymax : int
                Corners of the region to mask

            """
            self[xmin:xmax, ymin:ymax] = ma.masked

        def maskRegions(self, regions):
            """
            Mask several regions.

            Parameters
            ----------
            regions : list
                List of regions as in maskRegion.

            """
            for region in regions:
                self.maskRegion(*region)

        def maskbelowLine(self, p1, p2):
            """
            Mask points at one side of line.

            The masked side is left looking from p1 to p2.

            Parameters
            ----------
            p1, p2 : list of 2x float
                Points in pixel coordinates defining line.

            """
            points = np.stack([self.iX, self.iY])
            pp1 = np.array(p1)
            pp2 = np.array(p2)
            d = np.cross((pp1 - pp2)[:, None, None], pp2[:, None, None] - points, axis=0)
            self[d > 0] = ma.masked

        def maskTriangle(self, p1, p2, p3, invert=False):
            """
            Mask inside triangle.

            Parameters
            ----------
            p1,p2,p3 : list of 2x float
                Edge points of triangle.
            invert : bool
                Invert region. Mask outside circle.

            """
            points = np.stack([self.iX, self.iY], axis=2)
            pp1 = np.array(p1)
            pp2 = np.array(p2)
            pp3 = np.array(p3)
            # cross to get sides of lines
            d1 = np.sign(
                np.cross((pp1 - pp2)[None, None, :], points - pp2[None, None, :], axis=2).reshape(points.shape[0], -1))
            d2 = np.sign(
                np.cross((pp2 - pp3)[None, None, :], points - pp3[None, None, :], axis=2).reshape(points.shape[0], -1))
            d3 = np.sign(
                np.cross((pp3 - pp1)[None, None, :], points - pp1[None, None, :], axis=2).reshape(points.shape[0], -1))
            # equal side if sign equal sign of 3rd point
            mask = ((d1 == d1[p3[0], p3[1]]) & (d2 == d2[p1[0], p1[1]]) & (d3 == d3[p2[0], p2[1]]))
            if invert:
                self[~mask] = ma.masked
            else:
                self[mask] = ma.masked

        def mask4Polygon(self, p1, p2, p3, p4, invert=False):
            """
            Mask inside polygon of 4 points.

            Points need to be given in right hand order.

            Parameters
            ----------
            p1,p2,p3,p4 : list of 2x float
                Edge points.
            invert : bool
                Invert region. Mask outside circle.

            """
            points = np.stack([self.iX, self.iY], axis=2)
            pp1 = np.array(p1, dtype=np.int32)
            pp2 = np.array(p2, dtype=np.int32)
            pp3 = np.array(p3, dtype=np.int32)
            pp4 = np.array(p4, dtype=np.int32)
            # cross to get sides of lines
            d1 = np.sign(
                np.cross((pp1 - pp2)[None, None, :], points - pp2[None, None, :], axis=2).reshape(points.shape[0], -1))
            d2 = np.sign(
                np.cross((pp2 - pp3)[None, None, :], points - pp3[None, None, :], axis=2).reshape(points.shape[0], -1))
            d3 = np.sign(
                np.cross((pp3 - pp4)[None, None, :], points - pp4[None, None, :], axis=2).reshape(points.shape[0], -1))
            d4 = np.sign(
                np.cross((pp4 - pp1)[None, None, :], points - pp1[None, None, :], axis=2).reshape(points.shape[0], -1))
            # equal side if sign equal sign of 3rd point
            mask = ((d1 == d1[pp3[0], pp3[1]]) & (d2 == d2[pp4[0], pp4[1]]) & (d3 == d3[pp1[0], pp1[1]]) & (
                    d4 == d3[pp2[0], pp2[1]]))
            if invert:
                self[~mask] = ma.masked
            else:
                self[mask] = ma.masked

        def maskCircle(self, center, radius, invert=False):
            """
            Mask points inside circle.

            Parameters
            ----------
            center : list of 2x float
                Center point. This is not the plane center.
            radius : float
                Radius in pixel units
            invert : bool
                Invert region. Mask outside circle.

            """
            points = np.stack([self.iX, self.iY])
            distance = la.norm(points - np.array(center)[:, None, None], axis=0)
            mask = distance < radius
            if invert:
                self[~mask] = ma.masked
            else:
                self[mask] = ma.masked

        def maskSectors(self, angles, width, radialmax=None, invert=False):
            """
            Mask sector around center.

            Zero angle is

            Parameters
            ----------
            angles : list of float
                Center angles of sectors in grad.
            width : float or list of float
                Width of the sectors in grad.
                If single value all sectors are equal.
            radialmax : float
                Maximum radius in pixels.
            invert : bool
                Invert mask or not.

            Examples
            --------
            ::

             import jscatter as js
             cal = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             cal.maskSectors([-90,0,90,-180],20,radialmax=200,invert=True)
             cal.show()

            """
            angles = np.asarray(angles)

            if isinstance(width, numbers.Number):
                width = np.ones_like(angles) * width

            mask = self.mask.copy()
            mask[:] = False

            for a, w in zip(np.deg2rad(angles), np.deg2rad(np.abs(width))):
                limits = np.r_[a - w / 2, a + w / 2] % (2 * np.pi) - np.pi
                if radialmax is None:
                    if limits[0] < limits[1]:
                        mask = np.logical_or(mask, (self._polarazimuth > limits[0]) & (self._polarazimuth < limits[1]))
                    else:
                        mask = np.logical_or(mask, ~((self._polarazimuth < limits[0]) & (self._polarazimuth > limits[1])))
                else:
                    if limits[0] < limits[1]:
                        mask = np.logical_or(mask, (self._polarazimuth > limits[0]) & (self._polarazimuth < limits[1]) &
                                             (self._polarradial < radialmax))
                    else:
                        mask = np.logical_or(mask, ~((self._polarazimuth < limits[0]) & (self._polarazimuth > limits[1])) &
                                             (self._polarradial < radialmax))
            if invert:
                self[~mask] = ma.masked
            else:
                self[mask] = ma.masked

        def pQaxes(self):
            """
            Get scattering vector along detector pixel axes X, Y around center.

            In standard small angle geometry the detector pixel X,Y directions are perpendicular
            to the incident beam in Z direction. For an offset detector this not necessarily the case
            as we have curvilinear coordinates.
            Needs wavelength, detector_distance and center defined.

            Returns
            -------
                qx,qy with image x and y dimension

            """
            X = self.pQ[:, min(max(int(self.center[1]), 0), self.shape[1] - 1), 0]
            Y = self.pQ[min(max(int(self.center[0]), 0), self.shape[0] - 1), :, 1]
            return X, Y

        @property
        @sImemoize(4, ['center'])
        def _polarazimuth(self):
            """
            Polar pixel azimuth on detector plane.
            Orientation chosen that azimuth is same as pQrpt.
            """
            X = (self.iX - self.center[0])
            Y = (self.iY - self.center[1])
            return np.arctan2(Y, X)

        @property
        @sImemoize(4, ['center'])
        def _polarradial(self):
            """
            Polar pixel radial distance from center on detector plane.
            """
            X = (self.iX - self.center[0])
            Y = (self.iY - self.center[1])
            return np.linalg.norm([X, Y], axis=0)

        @property
        @sImemoize(4, ['center', 'detector_distance', 'pixel_size', 'alpha', 'beta', 'gamma'])
        def _pXYZ(self):
            """
            Cartesian pixel Coordinates of displaced detector.

            Use setDetectorPosition to place it.

            """
            # pixel positions (units m) in detector plane
            # with center as start point of plane normal vector pointing to the sample
            # For standard geometry normal is parallel to primary beam
            xyz = np.zeros(self.iX.shape + (3,))
            xyz[:, :, 0] = (self.iX - self.center[0]) * self.pixel_size[0]
            xyz[:, :, 1] = (self.iY - self.center[1]) * self.pixel_size[1]
            xyz[:, :, 2] = self.detector_distance[0]  # in  units m

            if self.alpha == 0 and self.beta == 0 and self.gamma == 0:
                return xyz
            else:
                # rotation along AXES all in intrinsic coordinates:
                # first rotate around Z (incoming beam) by gamma (will be rotation of detector normal around incoming beam )
                # then rotate detector plane by alpha around Y (alpha is between detector normal and incoming beam)
                # and by beta around Z (rotation of detector around detector plane normal)
                # create rotation matrix
                R = Rotation.from_euler('ZYZ', [self.gamma, self.alpha, self.beta], degrees=True).as_dcm()
                # rotate pixel coordinates
                Rxyz = np.einsum('ij,klj->kli', R, xyz)
                return Rxyz

        @property
        @sImemoize(4, ['center', 'detector_distance', 'pixel_size'])
        def _pXYZnorm(self):
            """
            Norm of Cartesian pixel Coordinates is distance from center

            Independent of rotation of detector if normal doesnt change

            """
            return la.norm(self._pXYZ, axis=-1)

        @property
        @sImemoize(4, ['center', 'detector_distance', 'pixel_size', 'alpha', 'beta', 'gamma', 'wavelength'])
        def pQ(self):
            r"""
            3D scattering vector :math:`q` for pixels with detector placed in standard SAS or offset geometry.

            The detector is placed at an offset position with the detector normal rotated
            against the incoming beam direction by angles alpha, beta, gamma.
            If these are zero we have conventional SAS geometry.

            Use .setDetectorPosition to place the detector.

            Returns
            -------
                array
                scattering vector with shape(image) x 3 in cartesian coordinates and units 1/nm.

            Notes
            -----
            The incident beam is directed in Z direction :math:`k_{i}=[0, 0, k]`.

            The scattered wavevector is

            .. math:: k_f=k \begin{bmatrix} cos(\phi)sin(\theta) \\ sin(\phi)sin(\theta) \\ cos(\theta) \end{bmatrix}
                         =  \begin{bmatrix} k_x \\ k_y \\ k_z \end{bmatrix}

            with polar coordinates :math:`k, \phi, \theta` where :math:`\theta` is the conventional scattering vector
            used in small angle scattering.

            The scattering vector is

            .. math:: q=k_f-k_i=
                k \begin{bmatrix} cos(\phi)sin(\theta) \\ sin(\phi)sin(\theta) \\ cos(\theta) -1 \end{bmatrix}

            with :math:`|q|=4\pi/\lambda` and :math:`|k|=2\pi/\lambda`.

            **Pixel and pQ orientation**

            The convention is as follows
             - Pixel origin [0,0] is upper left corner with coordinates in [height, width] of the detector.
             - pQ (wavevector coordinates) are that the pixel origin [0,0] is in the [positive, negative] quadrant
               that the lower left corner is [negative,negative] as expected for a scattering pattern
               with axes from left to right and bottom up and the origin (incident beam) somewhere
               in the image and viewed from the sample location.

            """

            # wavevector absolute value units  1/nm
            k = 2 * np.pi / (self.wavelength[0] / 10.)
            # scattered wavevector
            kf = self._pXYZ / self._pXYZnorm[:, :, None] * k
            # incident wavevector
            ki = np.r_[0, 0, k]
            kfki = kf - ki[None, None, :]
            # invert kfki x component to have later --quadrant in lower left corner
            kfki[:, :, 0] *= -1
            return kfki

        @property
        @sImemoize(4, ['center', 'detector_distance', 'pixel_size', 'alpha', 'beta', 'gamma', 'wavelength'])
        def pQnorm(self):
            """
            3D scattering vector :math:`|q|` for detector pixel. See .pQ .

            """
            return la.norm(self.pQ, axis=-1)

        @property
        @sImemoize(4, ['center', 'detector_distance', 'pixel_size', 'alpha', 'beta', 'gamma', 'wavelength'])
        def _pQrpt(self):
            r"""
            3D scattering vector :math:`q` for detector pixel in spherical coordinates
            :mat:`|q|`, phi, theta with laboratory z-axis along incident beam.

            In standard SAS geometry :math:`\theta \approx \pi/2 `.

            """
            return formel.xyz2rphitheta(self.pQ.reshape((-1, 3))).reshape(self.shape + (3,))

        def findCenterOfIntensity(self, center=None, size=100):
            """
            Find beam center as center of intensity around center.

            Only values above the mean value are used to calc center of intensity.
            Use an image with a clear symmetric and  strong scattering sample as AgBe.
            Use *.showPolar([600,699],scaleR=5)* to see if peak is symmetric.

            Parameters
            ----------
            center : list 2x int
                First estimate of center as [height, width] position.
                If not given preliminary center is estimated as center of intensity of full image.
            size : int
                Defines size of rectangular region of interest (ROI) around the center to look at.

            Returns
            -------
                Adds (replaces) .center as attribute.

            Notes
            -----
            If ROI is to large the result may be biased due to asymmetry of
            the intensity distribution inside of ROI.

            Additional strong masking in ROI leads to bias of the found center.

            """
            if isinstance(size, numbers.Numbers):
                size = np.rint(size).astype(np.int)
            med = (self.max() + self.min()).array / 2.
            if center is None:
                # as first guess
                center = ndimage.measurements.center_of_mass(ma.masked_less(self, med, copy=True).filled(0).array)
            if size is not None:
                # take smaller portion to reduce bias from image size
                bc = np.rint(center).astype(np.int)
                data = self[bc[0] - size:bc[0] + size, bc[1] - size:bc[1] + size]
                # mask values smaller than mean and take centerofmass
                med = (data.max() + data.min()).array / 2.
                ccenter = ndimage.measurements.center_of_mass(ma.masked_less(data, med, copy=True).filled(0).array)
                center = [ccenter[0] + bc[0] - size, ccenter[1] + bc[1] - size]
            self.setPlaneCenter(center)

        def radialAverage(self, center=None, number=300, kind='log', calcError=False, units=None):
            r"""
            Radial average of image and conversion to wavevector q.

            Remember to set .detector_distance to calibrated value.
            Setting units to 'sr'  will scale the output to counts/micro_steradians
            which also accounts for different detector distances.

            Parameters
            ----------
            center : list 2x float
                Sets center in data and uses this.
                If not given the attributecenter in the data is used.
            number : int, default 500
                Number of intervals on new X scale.
            kind : 'lin', default 'log'
                Determines how points are distributed.
            calcError : 'poisson','std', default None
                How to calculate error.
                 - 'poisson' according to Poisson statistics.
                    Use only for original images showing unprocessed photon counts.
                 - 'std' as standard deviation of the values in an interval.
                 - otherwise no error
            units : None, 'sr'
                Units of the returned radial average as :
                 - None default counts per pixel
                 - 'sr' counts per solid angle as counts/micro steradians = 1e-6 counts/steradians.
                   This accounts also different detector distances.

            Returns
            -------
                dataArray with added attributes of the image. artist and entriesXML are ommited

            Notes
            -----
            - Correction of pixel size for flat detector projected to Ewald sphere included.
              The correction is relative to the pixel located at the center. The intensity remains in units counts/pixel.
            - The value in a q binning is the average count rate :math:`c(q)=(\sum c_i)/N`
              with counts in pixel *i* :math:`c_i` and number of pixels :math:`N`
            - Setting units to 'sr' scales to counts per solid angle (micro steradians).
              In these units the scattered intensity is independent of the detector distance.
              Scaling is done after calculation of the error.

            - **calcError** :
              If the image is unprocessed (no background subtraction or transmission correction) containing  original
              photon count rates the standard error can be calculated from Poisson statistic.
               - The error (standard deviation) is calculated in a q binning as
                 :math:`e=(\sum c_i)^{1/2}/N`
               - The error is valid for single photon counting detectors showing Poisson statistics
                 as the today typical Pilatus detectors from DECTRIS.
               - The error for :math:`\sum c_i) <= 0` is set to zero.
                 One may estimate the corresponding error from neighboring intervals.
               - In later 1D processing as e.g. background correction
                 the error can be included according to error propagation.
              'std' calcs the error as standard deviation in an interval.

            Examples
            --------
            Mask and do radial average over sectors. ::

             import jscatter as js
             cal = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             p=js.grace()
             calc=cal.copy()
             calc.maskSectors([0,180],20,radialmax=100,invert=True)
             calc.show()
             icalc=calc.radialAverage()
             p.plot(icalc,le='horizontal')
             calc=cal.copy()
             calc.maskSectors([90+0,90+180],20,radialmax=100,invert=True)
             calc.show()
             icalc=calc.radialAverage()
             p.plot(icalc,le='vertical')
             p.yaxis(scale='l')
             p.legend()
             p.title('The AgBe is not isotropically ordered')

            """
            if center is not None:
                self.setPlaneCenter(center)

            # see Correcting for spherical angles
            # in Pauw, Everything SAXS:.... J. Phys.: Condens. Matter 25 (2013) 383201
            # correction for flat detector with pixel area as (distance pixel / distance to detector center)**3
            lpl0 = self._pXYZnorm / self.detector_distance[0]
            data = self.data * lpl0 ** 3
            mask = self.mask
            radial = dA(np.stack([self.pQnorm[~mask], data[~mask]]))
            radial.isort()  # sorts along X by default
            # return lower number of points from prune
            if calcError == 'poisson':
                result = radial.prune(number=number, type='sum', kind=kind)  # sum and number without error
                err = np.copy(result[1])
                result[1] = result[1] / result[2]  # calc mean
                err[err > 0] **= 0.5
                err[err <= 0] = 0
                result[2] = err / result[2]  # mean error
                result.setColumnIndex(iey=2)
            elif calcError == 'std':
                result = radial.prune(number=number, type='mean+std', kind=kind)  # average without error
            else:
                # no error
                result = radial.prune(number=number, type='mean', kind=kind)  # average without error
            result.filename = self.filename
            # add some attributes from image
            for attri in self.attr:
                if attri not in ['artist', 'entriesXML']:
                    try:
                        setattr(result, attri, getattr(self, attri))
                    except AttributeError:
                        pass

            if units == 'sr':
                # normalize to solid angle as pixel_area/r**2 in units micro steradians
                result[1:] = result[1:] / (self.pixel_size[0] * self.pixel_size[1] / self.detector_distance[0] ** 2 * 1e6)

            return result

        def azimuthAverage(self, center=None, qrange=[None, None], number=180, kind='lin', calcError=False):
            r"""
            Azimuthal average of image and conversion to wavevector q.

            Remember to set .detector_distance to calibrated value.

            Parameters
            ----------
            center : list 2x float
                Sets center in data and uses this.
                If not given the attribute center in the data is used.
            qrange : 2x float, default [0,max]
                Range of q values to include as [min,max].
            number : int, default 180
                Number of intervals on new X scale.
            kind : 'log', default 'lin'
                Determines how points are distributed.
            calcError : 'poisson','std', default None
                How to calculate error.
                 - 'poisson' according to Poisson statistics.
                    Use only for original images showing unprocessed photon counts.
                 - 'std' as standard deviation of the values in an interval.
                 - otherwise no error

            Returns
            -------
                dataArray with added attributes of the image. artist and entriesXML are ommited

            Notes
            -----
            - Correction of pixel size for flat detector projected to Ewald sphere included.
            - The value in a q binning is the average count rate :math:`c(q)=(\sum c_i)/N`
              with counts in pixel *i* :math:`c_i` and number of pixels :math:`N`

            - **calcError** :
              If the image is unprocessed (no background subtraction or transmission correction) containing  original
              photon count rates the standard error can be calculated from Poisson statistic.
               - The error (standard deviation) is calculated in a q binning as
                 :math:`e=(\sum c_i)^{1/2}/N`
               - The error is valid for single photon counting detectors showing Poisson statistics
                 as the today typical Pilatus detectors from DECTRIS.
               - The error for :math:`\sum c_i) <= 0` is set to zero.
                 One may estimate the corresponding error from neighboring intervals.
               - In later 1D processing as e.g. background correction
                 the error can be included according to error propagation.
              'std' calcs the error as standard deviation in an interval.

            Examples
            --------
            ::

             import jscatter as js
             cal = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             p=js.grace()
             az=cal.azimuthAverage(qrange=[0.9,1.3])
             p.plot(az,legend='q=0.9-1.3 nm\S-1')
             az=cal.azimuthAverage(qrange=[2,2.4])
             p.plot(az,legend='q=2-2.4 nm\S-1')
             az=cal.azimuthAverage(qrange=[3,3.5])
             p.plot(az,legend='q=3-3.5 nm\S-1')
             az=cal.azimuthAverage(qrange=[4.1,4.4])
             p.plot(az,legend='q=4.1-4.4 nm\S-1')

             p.yaxis(label=r'I(\xj\f{})',scale='log')
             p.xaxis(label=r'azimuth \xj\f{} / rad')
             p.title('The AgBe is not isotropically ordered')
             p.legend(x=0,y=1)
             p.text('beamstop arm',x=-2,y=0.1)
             p.text('mask',x=-3,y=4)

            """

            if center is not None:
                self.setPlaneCenter(center)

            # see Correcting for spherical angles
            # in Pauw, Everything SAXS:.... J. Phys.: Condens. Matter 25 (2013) 383201
            # correction for flat detector with pixel area as (distance pixel / distance to detector center)**3
            lpl0 = self._pXYZnorm / self.detector_distance[0]
            data = self.data * lpl0 ** 3
            mask = self.mask
            pQrpt = self._pQrpt

            if qrange[0] is None:   qrange[0] = np.min(pQrpt[:, :, 0])
            if qrange[1] is None:   qrange[1] = np.max(pQrpt[:, :, 0])

            mask = np.logical_or(mask, ~((pQrpt[:, :, 0] > qrange[0]) & (pQrpt[:, :, 0] < qrange[1])))
            radial = dA(np.stack([pQrpt[~mask, 1], data[~mask]]))
            radial.setColumnIndex(ix=0, iy=1, iey=None)
            radial.isort()  # sorts along X by default

            # return lower number of points from prune
            if calcError == 'poisson':
                result = radial.prune(number=number, type='sum', kind=kind)  # sum and number without error
                err = np.copy(result.Y)
                result.Y = result.Y / result[-1]  # calc mean
                err[err > 0] **= 0.5
                err[err <= 0] = 0
                result[-1] = err / result[-1]  # mean error
                result.setColumnIndex(iey=-1)
            elif calcError == 'std':
                result = radial.prune(number=number, type='mean+std', kind=kind)  # average without error
            else:
                # no error
                result = radial.prune(number=number, type='mean', kind=kind)  # average without error
            result.filename = self.filename
            # add some attributes from image
            for attri in self.attr:
                if attri not in ['artist', 'entriesXML']:
                    try:
                        setattr(result, attri, getattr(self, attri))
                    except AttributeError:
                        pass

            return result

        def lineAverage(self, center=None, number=None, minmax='auto', show=False):
            """
            Line average of image and conversion to wavevector q for line collimation cameras.

            Remember to set .detector_distance to calibrated value.

            Parameters
            ----------
            center : float
                Sets beam center in data and uses this.
                If not given the beam center is determined from semitransparent beam.
            number : int, default None
                Number of intervals on new X scale. None means all pixels.
            minmax : [int,int], 'auto'
                Interval for determination of center.
            show : bool
                Show the fit of the primary beam.

            Returns
            -------
                dataArray
                 - .filename
                 - .detector_distance
                 - .description
                 - .center

            Notes
            -----
            - Detector distance in attributes is used.
            - The primary beam is automatically detected.
            - Correction for flat detector projected to Ewald sphere included.

            """
            if center is None:
                # take average
                imageav = dA(np.c_[np.r_[0:self.shape[0]], self.mean(axis=1)].T)
                # find minima from argmax if not given explicitly
                if minmax[0] == 'a':  # auto
                    # for normal empty cell or buffer measurement the primary beam is the maximum
                    imax = imin = imageav.Y.argmax()
                    while imageav.Y[imax + 1] < imageav.Y[imax]:      imax += 1
                    while imageav.Y[imin - 1] < imageav.Y[imin]:      imin -= 1
                    xmax = imageav.X[imax]
                    xmin = imageav.X[imin]
                else:
                    xmin = minmax[0]
                    xmax = minmax[1]
                # prune to smaller interval
                primarybeam = imageav.prune(lower=xmin, upper=xmax)
                # subtract min value , which is basically dark current
                primarybeam.Y -= primarybeam.Y.min()
                norm = scipy.integrate.simps(primarybeam.Y, primarybeam.X)
                primarybeam.Y = primarybeam.Y / norm
                # fit mean position and width
                primarybeam.fit(_gauss, {'mean': imageav.Y.argmax(), 'sigma': 0.015, 'bgr': 0, 'A': 1}, {}, {'x': 'X'})
                center = primarybeam.mean
                self.primarybeam_hwhm = primarybeam.sigma * np.sqrt(np.log(2.0))
                self.primarybeam_peakmax = primarybeam.modelValues(x=primarybeam.mean).Y[0] * norm
                if show:
                    primarybeam.showlastErrPlot()
            self.center = center
            r = (self.iX[0] - self.center) * self.pixelSize  # µm pixel size
            # calc radial wavevectors
            angle = np.arctan(r / self.detector_distance[0])
            wl = self.wavelength[0]
            self.q = 4 * np.pi / wl * np.sin(angle / 2)
            # correction for flat detector with pixel area
            lpl0 = 1. / np.cos(angle)
            data = self.mean(axis=0) * lpl0  # because of line collimation only power 1
            error = self.std(axis=0) * lpl0  # because of line collimation only power 1
            result = dA(np.stack([self.q, data, error]))
            if number is not None:
                # return lower number of points from prune
                result = result.prune(number=number, kind='mean+')  # makes averages with errors
            result.filename = self.filename
            result.detector_distance = self.detector_distance
            result.description = self.description
            return result

        def recalibrateDetDistance(self, center=None, number=500, fcenter=1., fwhm=0.1, showfits=False):
            """
            Recalibration of detectorDistance by AgBe reference for point collimation.

            Use only for AgBe reference measurements to determine the correction factor.
            For non AgBe measurements set during reading or .detector_distance to the new value.
            May not work if the detector distance is totally wrong.

            Parameters
            ----------
            center : list 2x float
                Sets beam center or radial center in data and uses this.
                If not given the attributecenter in the data is used.
            number : int, default 1000
                number of intervals on new X scale.
            fcenter : float, default 1
                Determines start value for peak fitting.

                By default the position of the peak maximum is used if it is larger than
                (mean(Y)+2std(Y)) of the signal Y. Otherwise fcenter*peakposition[i] is used.
                Negative fcenter forces the start value to be |fcenter|*peakposition[i].

                Reference peakpositions in 1/nm:
                 [1.0753, 2.1521, 3.2286, 4.3049, 5.3813, 6.4576, 7.5339, 8.6102, 9.6865, 10.7628]
            fwhm : float, default 0.1
                Start value for full width half maximum in peak fitting.
            showfits : bool
                Show the AgBe peak fits.

            Notes
            -----
            - .distanceCorrection will contain factor for correction.
              Repeating this results in a .distanceCorrection close to 1.

            We fit a Voigt function to each of the detected peaks in the image
            and use the average of the resulting correction factors for each peak as overall correction factor.

            """
            # do radial average
            iq = self.radialAverage(center=center, number=number)
            # later distance corrections
            self.distanceCorrection = []
            dq = 0.3  # around peak positions
            for agp in AgBepeaks:
                # AgBepeaks contains a list of AgBe peak positions to test for
                # we fit each with a voigt function and take later the average
                if iq.X.max() > agp + dq and iq.X.min() < agp - dq:
                    # cut between lower and upper and fit Voigt function for peak
                    iqq = iq.prune(lower=agp - dq, upper=agp + dq, weight=None)
                    # iqq.setColumnIndex(iey=None)
                    if iqq.shape[1] < 5:
                        continue
                    iqq.setLimit(amplitude=[0], bgr=[0], fwhm=[0.001, agp])
                    if (iqq.Y.argmax() > iqq.Y.mean() + 2 * iqq.Y.std()) and (fcenter > 0):
                        centerstart = iqq.X[iqq.Y.argmax()]
                    else:
                        centerstart = agp * abs(fcenter)

                    ret = iqq.fit(_agbpeak, {'center': centerstart, 'amplitude': iqq.Y.max() / 4.,
                                             'fwhm': abs(fwhm), 'asym': 1, 'bgr': iqq.Y.min() / 2.},
                                  {}, {'q': 'X'}, output=False)
                    if ret == -1:
                        continue
                    if showfits:
                        iqq.showlastErrPlot()
                    self.distanceCorrection += [iqq.center / agp]
            corfactor = np.mean(self.distanceCorrection)
            corstd = np.std(self.distanceCorrection) / corfactor
            # set new detectorDistance
            self.detector_distance[0] *= corfactor
            print('\nCorrection factor {0:.4f} to new distance {1:.4f} (rel error : {2:.4f} )'.
                  format(corfactor, self.detector_distance[0], corstd))

        def calibrateOffsetDetector(self, lattice=None, center=None, distance=None, alpha=None, beta=None, gamma=None,
                                    domainsize=1000, asym=0, lg=1, rmsd=0.02, hklmax=17):
            """
            Compare sasImage to calibration standard in powder average to determine detector position.

            Any detector orientation relative to the sample and incoming beam can be used to
            calibrate parameters describing the detector position.
            A standard sample as silicon (large angles) or AgBe (small angles) can be used.
            Opens a window with changeable parameters to align sasImage and simulated lattice image.
            This can also be used in standard SAS geometry with the detector normal being the incoming beam direction.

            Parameters
            ----------
            lattice : lattice object
                A lattice object (see structurefactor lattices) representing the used
                reference material to determine the expected scattering pattern.
                E.g.  :code:`silicon = js.sf.diamondLattice(0.543, 1)`
            center,distance,alpha,beta,gamma : 2x float, float, float,float
                Parameters determining the detector position.
                See sasImage.setDetectorPosition for detailed description.
            domainsize : float
                Domainsize of the used powder changing the peak width. See sf.latticeStructureFactor.
            asym : float
                Asymmetry of the peaks. See sf.latticeStructureFactor.
            lg : float
                Lorenzian/gaussian fraction. See sf.latticeStructureFactor.
            rmsd : float
                Root mean square displacement in lattice . See sf.latticeStructureFactor.
            hklmax : int, default = 17
                Maximum order of the Bragg peaks to include.
                If hklmax is to small bragg peaks of higher order might be missing.

            Returns
            -------
                None
                Sets corresponding values in setDetectorPosition(...) when window is closed.

            Notes
            -----
            Usage:
            - Opens a window with
              1. Original gray scale image of calibration measurements with colored overlay for simulated peak positions.
              2. A radial average over the image and simulated data.
                 The radial average depends on center location and angles and is updated if parameters change.

            - The overlay in left image shows the range between selected doted lines in right image.
              - The selection is done using the right/left mouse button for max/min values.
              - Selection might highlight the maxima of peaks or the flanks dependent on the selected range.
                Small ranges highlight thin lines in the 2D image.

            - Lines have to be aligned to measured pattern and peak positions need to coincident.

            - Use the calibrated sasImage as argument to copy while creating a new sasImage to use (copy)
              calibrated values in new image.

            - A detector geometry as 45deg with the beamcenter at the detector edge might be used to cover
              a large  region from lowest to highest wavevectors.

            Examples
            --------
            Silicon powder for an offset detector located parallel to the incoming beam in reverse orientation (beta=90)
            The detector was positioned about 70 mm away from the incoming beam a bit behind the sample.
            Here the distance and center need to be adjusted.
            ::

             import jscatter as js
             sic = js.sas.sasImage(js.examples.datapath+'/Silicon.tiff')
             silicon = js.sf.diamondLattice(0.543, 1)
             cc=[130,-100]
             sic.setPlaneOrientation(90,90,0)
             sic.calibrateOffsetDetector(center=cc,distance=0.070,lattice=silicon,domainsize=30,rmsd=0.003)

            Conventional AgBe reference with center located on the detector.
            Beamcenter is center on detector.
            ::

             import jscatter as js
             #
             cal = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             # mask some parts of the detector (because of shading and the beam stop) to get clearer radial average.
             bc=cal.center
             # beamstop arm
             cal.mask4Polygon([bc[0]+5,bc[1]],[bc[0]-5,bc[1]],[bc[0]-5+43,0],[bc[0]+5+43,0])
             # beamstop
             cal.maskCircle(cal.center, 18)
             # shade of beam entrance
             cal.maskCircle([500,320], 280,invert=True)
             # AgBe reference
             agbe=js.sf.latticeFromCIF(js.examples.datapath+'/1507774.cif',size=[1,1,1])
             cal.calibrateOffsetDetector(center=cal.center,distance=0.18,lattice=agbe,domainsize=50,rmsd=0.003)

            """
            if mpl._headless:
                warnings.warn('calibrateOffsetDetector cannot be used in headless mode!')
                return
            # change detector position
            self.setDetectorPosition(center, distance, alpha=alpha, beta=beta, gamma=gamma)

            origin = 'lower'
            bfontsize = 8
            extend = None

            normdif = colors.LogNorm(clip=True)

            figsize = pyplot.figaspect(0.35)
            fig = pyplot.figure(figsize=figsize)
            # axes
            axdif = fig.add_axes([0.05, 0.1, 0.4, 0.75])
            axline = fig.add_axes([0.46, 0.12, 0.5, 0.66])

            # arrange buttons
            xx = 0.46
            yy = 0.79
            dx = 0.06
            dy = 0.05
            dl = 0.05
            dly = 0.045
            # center
            axcenterx_p = pyplot.axes([xx + dx / 2, yy + dy * 2, dl, dly])
            bcenterx_p = Button(axcenterx_p, 'center X+', color='linen', hovercolor='green')
            bcenterx_p.label.set_fontsize(bfontsize)
            axcenterx_m = pyplot.axes([xx + dx / 2, yy, dl, dly])
            bcenterx_m = Button(axcenterx_m, 'center X-', color='linen', hovercolor='green')
            bcenterx_m.label.set_fontsize(bfontsize)
            axcentery_p = pyplot.axes([xx + dx, yy + dy, dl, dly])
            bcentery_p = Button(axcentery_p, 'center Y+', color='linen', hovercolor='green')
            bcentery_p.label.set_fontsize(bfontsize)
            axcentery_m = pyplot.axes([xx, yy + dy, dl, dly])
            bcentery_m = Button(axcentery_m, 'center Y-', color='linen', hovercolor='green')
            bcentery_m.label.set_fontsize(bfontsize)
            # center step
            acstep = pyplot.axes([xx + 0.5 * dx, yy + 3 * dy, dl, dly])
            bcstep = Button(acstep, r'step $\pm$1', color='linen', hovercolor='green')
            bcstep.label.set_fontsize(bfontsize)

            # detector distance
            axdistance_p = pyplot.axes([xx + 2 * dx, yy + dy, dl, dly])
            bdistance_p = Button(axdistance_p, 'distance +', color='grey', hovercolor='green')
            bdistance_p.label.set_fontsize(bfontsize)
            axdistance_m = pyplot.axes([xx + 2 * dx, yy, dl, dly])
            bdistance_m = Button(axdistance_m, 'distance -', color='grey', hovercolor='green')
            bdistance_m.label.set_fontsize(bfontsize)
            # alpha
            axalpha_p = pyplot.axes([xx + 3. * dx, yy + dy, dl, dly])
            balpha_p = Button(axalpha_p, 'alpha +', color='lightgrey', hovercolor='green')
            balpha_p.label.set_fontsize(bfontsize)
            axalpha_m = pyplot.axes([xx + 3. * dx, yy, dl, dly])
            balpha_m = Button(axalpha_m, 'alpha -', color='lightgrey', hovercolor='green')
            balpha_m.label.set_fontsize(bfontsize)
            # beta
            axbeta_p = pyplot.axes([xx + 4. * dx, yy + dy, dl, dly])
            bbeta_p = Button(axbeta_p, 'beta +', color='lightgrey', hovercolor='green')
            bbeta_p.label.set_fontsize(bfontsize)
            axbeta_m = pyplot.axes([xx + 4. * dx, yy, dl, dly])
            bbeta_m = Button(axbeta_m, 'beta -', color='lightgrey', hovercolor='green')
            bbeta_m.label.set_fontsize(bfontsize)
            # gamma
            axgamma_p = pyplot.axes([xx + 5. * dx, yy + dy, dl, dly])
            bgamma_p = Button(axgamma_p, 'gamma +', color='lightgrey', hovercolor='green')
            bgamma_p.label.set_fontsize(bfontsize)
            axgamma_m = pyplot.axes([xx + 5. * dx, yy, dl, dly])
            bgamma_m = Button(axgamma_m, 'gamma -', color='lightgrey', hovercolor='green')
            bgamma_m.label.set_fontsize(bfontsize)

            # distance step
            axstep = pyplot.axes([xx + 2 * dx, yy + 3 * dy, dl, dly])
            bstep = Button(axstep, r'step $\pm$10', color='grey', hovercolor='green')
            bstep.label.set_fontsize(bfontsize)
            # angular step
            aastep = pyplot.axes([xx + 4. * dx, yy + 3 * dy, dl, dly])
            bastep = Button(aastep, r'step $\pm$3', color='lightgrey', hovercolor='green')
            bastep.label.set_fontsize(bfontsize)

            # buttonlist for picker
            buttons = [bstep, bcenterx_p, bcenterx_m, bcentery_p, bcentery_m, bdistance_p, bdistance_m,
                       balpha_p, balpha_m, bbeta_p, bbeta_m, bgamma_p, bgamma_m, bastep, bcstep]

            axdif.set_title(
                'Move detector position and orientation according to buttons. '
                '\n to align rings to measured pattern.\n'
                ' intervals (dots) are set by left/right mouse click in right plot.',
                fontsize=9)

            # preliminary show something in picker, it is immediately updated
            # looking at self
            selfradial = self.radialAverage()
            axline.plot(selfradial.X, selfradial.Y)
            bgr = selfradial.Y.min()

            # looking at the lattice
            Iradial = sf.radial3DLSF(self.pQ.reshape(-1, 3), lattice=lattice, domainsize=domainsize, asym=asym,
                                     lg=lg, rmsd=rmsd, beta=None, hklmax=hklmax, wavelength=self.wavelength[0] / 10)
            I2d = Iradial.Y.reshape(self.shape)
            scaling = (selfradial.Y.max() - bgr) / (Iradial.Y.max() - Iradial.Y.min())
            I2d = I2d * scaling + bgr
            I2d[self.mask] = bgr
            latticeradial = sf.latticeStructureFactor(selfradial.X, lattice=lattice, domainsize=domainsize, asym=asym,
                                                      lg=lg, rmsd=rmsd, beta=None, hklmax=hklmax,
                                                      wavelength=self.wavelength[0] / 10)
            scaling = (selfradial.Y.max() - bgr) / (latticeradial.Y.max() - latticeradial.Y.min())
            axline.plot(latticeradial.X, latticeradial.Y * scaling + bgr)
            axline.plot(latticeradial.X, np.ones_like(latticeradial.Y) * latticeradial.Y.max() * 0.9 * scaling + bgr,
                        linestyle=':')
            axline.plot(latticeradial.X, np.ones_like(latticeradial.Y) * latticeradial.Y.max() * 0.1 * scaling + bgr,
                        linestyle=':')

            normdif.autoscale(self)
            imdif1 = axdif.imshow(self, cmap='gray', extent=extend, origin=origin, norm=normdif)
            imdif2 = axdif.imshow(I2d, cmap='Wistia', extent=extend, origin=origin)
            axdif.images[1].cmap.set_bad(alpha=0)
            axdif.images[1].cmap.set_under(alpha=0)
            axdif.images[1].cmap.set_over(alpha=0.2)
            fig.colorbar(imdif1, ax=axdif, pad=0.0)
            fig.text(x=xx + 6. * dx, y=yy + 1 * dy, s='center \ndistance  ')
            axdif.invert_yaxis()
            axdif.set_xlabel('Y dimension / pixel')
            axdif.set_ylabel('X dimension / pixel')
            axline.set_yscale('log')
            axline.set_xlabel(r'Q / $nm^{-1}$')

            # create picker and turn matplotlib to blocking mode to wait until window is closed
            pick = PickerDetPosition(image=self, lattice=lattice, axes=[axdif, axline], buttons=buttons,
                                     latticeparameters=[domainsize, asym, lg, rmsd, hklmax])

            mpl.pyplot.show(block=True)
            # If the picker is closed the detector position is already updated to the last values

            return

        # noinspection PyIncorrectDocstring
        def show(self, **kwargs):
            r"""
            Show sasImage as matplotlib figure.

            Parameters
            ----------
            scale : 'log', 'symlog', default = 'norm'
                Scale for intensities.

                - 'norm' Linear scale.
                - 'log' Logarithmic scale
                - 'symlog' Symmetrical logarithmic scale is logarithmic in both the positive
                  and negative directions from the origin. This works also for only positive data.
                  Use linthresh, linscale to adjust.
            levels : int, None
                Number of contour levels.
            colorMap : string
                Get a colormap instance from name.
                Standard mpl colormap name (see showColors).
            badcolor : float, color
                Set the color for bad values (like masked) values in an image.
                Default is  bad values be transparent.
                Color can be matplotlib color as 'k','b' or
                float value in interval [0,1] of the chosen colorMap.
                0 sets to minimum value, 1 to maximum value.
            linthresh : float, default = 1
                Only used for scale 'symlog'.
                The range within which the plot is linear (-linthresh to linthresh).
            linscale : float, default = 1
                Only used for scale 'symlog'.
                Its value is the number of decades to use for each half of the linear range.
                E.g. 10 uses 1 decade.
            lineMap : string
                Label color
                Colormap name as in colorMap, otherwise as cs in in Axes.clabel
                * if None, the color of each label matches the color of the corresponding contour
                * if one string color, e.g., colors = ‘r’ or colors = ‘red’, all labels will be plotted in this color
                * if a tuple of matplotlib color args (string, float, rgb, etc),
                  different labels will be plotted in different colors in the order specified
            fontsize : int, default 10
                Size of line labels in pixel
            axis : None, 'pixel'
                If coordinates should be forced to pixel, otherwise wavevectors if possible.
            invert_yaxis, invert_xaxis : bool
                Invert corresponding axis.
            block : bool
                Open in blocking or non-blocking mode
            origin : 'lower','upper'
                Origin of the plot. See matplotlib imshow.

            Returns
            -------
                image handle

            Notes
            -----
            To show data as Image in correct orientation the option ax.invert_yaxis() is used.
            If you plot directly with matplotlib use the same.

            Examples
            --------
            Use radial averaged data to interpolate ::

             import jscatter as js
             import numpy as np
             calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             calibration.show(colorMap='ocean')
             calibration.show(scale='sym',linthresh=20, linscale=5)

            Use ``scale='symlog'`` for mixed lin-log scaling to pronounce low scattering.
            See mpl.contourImage for more options also available using ``.show``. ::

             import jscatter as js
             import numpy as np
             # sets negative values to zero
             bsa = js.sas.sasImage(js.examples.datapath+'/BSA11mg.tiff')
             fig=js.mpl.contourImage(bsa,scale='sym',linthresh=30, linscale=10)
             fig.axes[0].set_xlabel(r'$Q_{{ \mathrm{{X}} }}\;/\;\mathrm{{nm^{{-1}}}}$ ')
             fig.axes[0].set_ylabel(r'$Q_{{ \mathrm{{Y}} }}\;/\;\mathrm{{nm^{{-1}}}}$ ')

            """
            block = kwargs.pop('block', False)
            kwargs.update({'block': False})

            axis = kwargs.pop('axis', None)
            if axis != 'pixel':
                try:
                    # if wavevectors are working we name them
                    _ = self.pQaxes()
                    unit = r'$Q_{{ \mathrm{{ {0:s}}} }}\;/\;\mathrm{{nm^{{-1}}}}$ '
                except AttributeError:
                    unit = '{0:s} pixel'
                    kwargs.update({'axis': 'pixel'})
            else:
                unit = '{0:s} pixel'
                kwargs.update({'axis': 'pixel'})

            fig = mpl.contourImage(x=self, **kwargs)

            fig.axes[0].set_xlabel(unit.format('Y'))
            fig.axes[0].set_ylabel(unit.format('X'))
            mpl.show(block=block)
            return fig

        def gaussianFilter(self, sigma=2):
            """
            Gaussian filter in place.

            Uses ndimage.filters.gaussian_filter with default parameters except sigma.

            Parameters
            ----------
            sigma : float
                Gaussian kernel sigma.

            """
            self[self.mask] = ndimage.filters.gaussian_filter(self.data, sigma)[self.mask]

        def reduceSize(self, bin=2, center=None, border=None):
            """
            Reduce size of image using uniform average in box.

            Center, pixel_size are scaled correspondingly.

            Parameters
            ----------
            bin : int
                Size of box to average within.
                Also factor for reduction in image size.
            center : [int,int]
                Center of crop region.
            border : int
                Size of crop region.
                 - If center is given a box with 2*size around center is used.
                 - If center is None the border is cut by size.

            Returns
            -------
                sasImage

            """
            i1 = i3 = 0
            i2 = i4 = 100000

            if border is not None:
                # set box around center or from border
                if center is not None:
                    center = np.asarray(center, int)
                    i1 = center[0] - border
                    i2 = center[0] + border
                    i3 = center[1] - border
                    i4 = center[1] + border
                else:
                    i1 = border
                    i2 = self.shape[0] - border
                    i3 = border
                    i4 = self.shape[1] - border

            data = self[max(i1, 0):min(i2, self.shape[0]), max(i3, 0):min(i4, self.shape[1])].copy()
            data[data.mask] = ndimage.filters.uniform_filter(data.data, size=bin)[data.mask]
            smalldata = data[::bin, ::bin]
            # clear caches

            # copy attributes
            smalldata.setAttrFromImage(self)

            # change some attributes according to bin
            try:
                # increase pixel size
                smalldata.pixel_size = [pz * bin for pz in smalldata.pixel_size]
            except AttributeError:
                pass
            try:
                bc = self.center[:]
                bc[0] = (bc[0] - max(i1, 0)) / bin
                bc[1] = (bc[1] - max(i3, 0)) / bin
                smalldata.setPlaneCenter(bc)
            except AttributeError:
                pass

            # set pixel coordinates
            smalldata.ImageWidth = [smalldata.shape[1]]
            smalldata.ImageLength = [smalldata.shape[0]]
            try:
                # for some images the XYResolution if it exists
                smalldata.XResolution[0] = data.XResolution[0] * float(bin)
                smalldata.YResolution[0] = data.YResolution[0] * float(bin)
            except AttributeError:
                pass
            smalldata._setEXIF()
            return smalldata

        def getPolar(self, center=None, scaleR=1, offset=0):
            """
            Transform to polar coordinates around center with interpolation.

            Azimuth corresponds:
            center line upwards, upper quarter center to right
            upper/lower edge = center downwards, lower quarter center to left

            Parameters
            ----------
            center : [int,float]
                Beamcenter
            scaleR : float
                Scaling factor for radial component to zoom.
                Works only for alpha,beta,gamma = 0 .
            offset : float >0
                Offset to remove center from polar image.
                Works only for alpha,beta,gamma = 0 .

            Returns
            -------
                ndarray

            Examples
            --------
            See showPolar for examples.

            Use radial averaged data to interpolate ::

             import jscatter as js
             calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             pol = calibration.getPolar()

            """
            if center is not None:
                self.setPlaneCenter(center)

            def transformsas(pr, bc, shape, scale, offset, rptmin, rptwidth):
                """
                Transform polar coordinates to cartesian with bc shift and scaling of radial component to magnify
                This is the simple case of standard SAS geometry.
                Calculation is done in pixel coordinates.
                """
                phi = rptmin[1] + (pr[0] / shape[0] * rptwidth[1])
                r = (rptmin[0] + offset + pr[1]) / scale
                return r * np.cos(phi) + bc[0], r * np.sin(phi) + bc[1]

            rptmin = self._pQrpt.min(axis=(0, 1))
            rptmax = self._pQrpt.max(axis=(0, 1))
            rptwidth = rptmax - rptmin
            if self.alpha == 0 and self.beta == 0 and self.gamma == 0:
                newimage = np.zeros_like(self.data)
                # use edges and width to transform pixel coordinates to new image
                ndimage.geometric_transform(self, mapping=transformsas, output=newimage,
                                            extra_keywords={'bc': self.center,
                                                            'scale': scaleR,
                                                            'offset': abs(offset),
                                                            'shape': self.shape,
                                                            'rptmin': rptmin,
                                                            'rptwidth': rptwidth})
                return newimage
            else:
                points = self._pQrpt[~self.mask][:, :2]
                values = self[~self.mask]
                gridx, gridy = np.mgrid[rptmin[1]:rptmax[1]:self.shape[0] * 1j, rptmin[0]:rptmax[0]:self.shape[1] * 1j]
                ndInterpol = LinearNDInterpolator(points=points, values=values, fill_value=0, rescale=True)
                newimage = ndInterpol(gridy, gridx)

                return newimage + 1

        def showPolar(self, center=None, scaleR=1, offset=0, scale='log'):
            """
            Show image transformed to polar coordinates around center.

            Azimuth for standard SAS geometry corresponds to:
            center line upwards, upper quarter center to right
            upper/lower edge = center downwards, lower quarter center to left

            Parameters
            ----------
            center : [int,int]
                Beamcenter
            scaleR : float
                Scaling factor for radial component to zoom the center.
                Works only for alpha,beta,gamma = 0 .
            offset : float
                Offset to remove center from polar image.
                Works only for alpha,beta,gamma = 0 .
            scale : 'log', 'symlog', default = 'log'
                Scale for intensities.

                - 'norm' Linear scale.
                - 'log' Logarithmic scale
                - 'symlog' Symmetrical logarithmic scale is logarithmic in both the positive
                  and negative directions from the origin. This works also for only positive data.
                  Use linthresh, linscale to adjust.

            Returns
            -------
                Handle to figure

            Examples
            --------
            Use polar coordinates to see if center is in middle of Debye-Scherrer rings.
            First standard SAS geometry::

             import jscatter as js
             calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             calibration.showPolar()

            For offset detector ::

             import jscatter as js
             import numpy as np

             sic = js.sas.sasImage(js.examples.datapath+'/Silicon.tiff')
             sic.setDetectorPosition([130,-100],0.070,90,90,0)
             sic.showPolar()

            """
            newimage = self.getPolar(center=center, scaleR=scaleR, offset=offset)

            f = mpl.contourImage(newimage, axis='pixel', scale=scale)
            f.axes[0].set_ylabel('azimuth / pixel')
            f.axes[0].set_xlabel('radial / pixel')
            mpl.show(block=False)

            return f

        def asdataArray(self, masked=0):
            """
            Return representation of sasImage as dataArray representing wavevectors (qx,qy) against intensity.

            Parameters
            ----------
            masked : float, None, string, default=0
                How to deal with masked values.
                 - float : Set masked pixels to this value
                 - None  : Remove from dataArray.
                           To recover the image the masked pixels need to be interpolated on a regular grid.
                 - ‘linear’, ‘cubic’, ‘nearest’ : interpolate masked points by scipy.interpolate.griddata
                                                  using specified order of interpolation on 2D image.
                 - 'radial' Use the radial averaged data to interpolate.

            Returns
            -------
             dataArray with [qx, qy, qz, I(qx,qy,qz)]
                - .qx, .qy : original q pixel values corresponding to image pixels along axes through the center
                  to recover the image. (.qx for .qy=0 and .qy for .qx=0.)
                  Please keep in mind that the values are only exact for integer center values.
                  Values are also not equidistant as for larger values the curvature of the Ewald sphere is important.
                  To recover the image use .regrid(method='nearest') to avoid artefacts due to this inaccuracy
                  and mask values according to original mask.
                - .sasImageshape as shape of original sasImage
                - Image attributes except ['artist', 'imageDescription'] are copied.
                - [qx,qy,qz] correspond to [.X, .Z, .W] in dataArray with .Y as I(qx,qy,qz).
                - The third dimension .qz (.W in dataArray) results from the fact that the flat detector image represents
                  the scattering vectors on the Ewald sphere which has also a qz component.

                  For small angle scattering this component might be small compared to qx,qy.

            Examples
            --------
            Use radial averaged data to interpolate the regions where the detector is dark ::

             import jscatter as js
             import numpy as np
             bsa = js.sas.sasImage(js.examples.datapath+'/BSA11mg.tiff')
             bsa.maskCircle(bsa.center,40)
             bsar=bsa.asdataArray('radial')
             js.mpl.surface(bsar.X, bsar.Z, bsar.Y,shape=bsar.sasImageshape)

            This demo will show the interpolation in the masked regions of an artificial intensity distribution.
            The examples might allow interpolation in a masked region like a unwanted Bragg reflex::

             import jscatter as js
             import numpy as np
             calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             # manipulate data (not the mask)
             # this only creates here a flat plateau for later interpolation.
             calibration.data[:300,60:1200]=100
             calibration.data[:300,120:180]=300
             calibration.data[:300,180:]=600
             # mask a circle
             calibration.maskCircle([200,200], 120)
             cal=calibration.asdataArray('linear')
             cal.Y[cal.Y<=0.1]=1.1
             js.mpl.surface(cal.X, cal.Z, cal.Y,shape=cal.sasImageshape)

             cal2=calibration.asdataArray(None)  # this is reduced in size due to the mask

            """
            qxzw = self.pQ.reshape((-1, 3))  # array of qx and qz, qw
            # return flat array without masked data
            mask = ~self.mask.flatten()
            if isinstance(masked, numbers.Number):
                out = dA(np.vstack([qxzw.T, self.data.flatten()]), XYeYeX=[0, 3, None, None, 1, None, 2, None])
                out[3, ~mask] = masked
            elif isinstance(masked, str) and self.mask.sum() > 0:
                if masked not in ['linear', 'cubic', 'nearest', 'radial']:
                    masked = 'nearest'
                ix = self.iX.flatten()
                iy = self.iY.flatten()
                dat = self.data.flatten()
                if masked[0] != 'r':
                    # 2D interpolation for masked points along pixel numbers
                    f = griddata(np.stack([ix[mask], iy[mask]], axis=1), dat[mask],
                                 (ix[~mask], iy[~mask]), method=masked)
                    dat[~mask] = f
                    out = dA(np.vstack([qxzw.T, dat]), XYeYeX=[0, 3, None, None, 1, None, 2, None])
                else:
                    # interpolate from radial averaged image
                    qr = la.norm(qxzw, axis=-1)
                    # radial averaged data as dataarray with interp function
                    radial = self.radialAverage()
                    dat[~mask] = radial.interp(qr[~mask])
                    out = dA(np.vstack([qxzw.T, dat]), XYeYeX=[0, 3, None, None, 1, None, 2, None])
            else:
                out = dA(np.vstack([qxzw[mask, :].T, self.flatten()[mask]]), XYeYeX=[0, 3, None, None, 1, None, 2, None])

            for attr in self.attr:
                if attr not in ['artist', 'imageDescription']:
                    setattr(out, attr, getattr(self, attr))
            out.qx, out.qy = self.pQaxes()
            # out.qx = qxzw[:, 0]
            # out.qy = qxzw[:, 1]
            out.sasImageshape = self.shape
            return out

        def interpolateMaskedRadial(self, radial=None):
            """
            Interpolate masked values from radial averaged image or function.

            This can be used to "extrapolate" over masked regions if e.g a background was measured
            at wrong distance.

            Parameters
            ----------
            radial : dataArray, function, default = None
                Determines how to determine masked values based on radial *q* values from center.
                 - Function accepting array to calculate masked data.
                 - dataArray for linear interpolating masked points.
                 - None uses the radialAverage image.

            Returns
            -------
                sasImage including original parameters.

            Examples
            --------
            Use radial averaged data to interpolate ::

             import jscatter as js
             import numpy as np
             calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             cal=calibration.interpolateMaskedRadial()
             # or
             # cal=calibration.interpolateMaskedRadial(calibration.radialAverage())
             cal.show()

            Generate image for different detector distance ::

             cal.setDetectorDistance(0.3)
             # mask whole image
             cal.mask=np.ma.masked
             # recover image with radial average from original
             cal2=cal.interpolateMaskedRadial(calibration.radialAverage())
             cal2.show()

            """
            qxzw = self.pQ.reshape((-1, 3))  # array of qx and qz, qw
            # return flat array without masked data
            mask = self.mask.flatten()
            dat = self.data.flatten()
            qr = la.norm(qxzw, axis=-1)
            if formel._getFuncCode(radial):
                # is a function to call
                dat[mask] = radial(qr[mask])
            elif radial is None:
                # radial average in q units
                radial = self.radialAverage()
                dat[mask] = radial.interp(qr[mask])
            elif hasattr(radial, '_isdataArray'):
                dat[mask] = radial.interp(qr[mask])
            else:
                raise Exception('Unknown radial ')
            image = self.copy()
            image.data[self.mask] = dat.reshape(self.shape[0], self.shape[1])[self.mask]
            image.maskReset()
            return image

        def saveAsTIF(self, filename, fill=None, **params):
            """
            Save the sasImage as float32 tif without loosing information.

            Conversion from float64 to float32 is necessary.
            To save colored images use asImage.save() (see :py:func:`~sasImage.asImage`)

            Parameters
            ----------
            filename : string
                Filename to save to.
            fill : float, 'min' default None
                Fill value for masked values. By default this is -1.

                'min' uses the minimal value of the respective data type
                 - np.iinfo(np.int32).min = -2147483648  for int32
                 - np.finfo(np.float32).min = -3.4028235e+38 for float32
            params : kwargs
                Additional kwargs for PIL.Image.save if needed.

            Examples
            --------
            ::

             import jscatter as js
             cal = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             cal2=cal/2.
             cal2.saveAsTIF('mycal',fill=-100)
             mycal=js.sas.sasImage('mycal.tif',maskbelow=-200)
             mycal.show()

            """
            # create image
            _, file_extension = os.path.splitext(filename)
            if file_extension not in ['.tif', '.tiff']:
                filename += '.tif'
            if fill is None:
                fill = -1
            if self.dtype == 'int32':
                if fill == 'min':
                    fill = np.iinfo(np.int32).min
                image = PIL.Image.fromarray(self.filled(fill), mode='I')
            else:  # as float32
                if fill == 'min':
                    fill = np.finfo(np.float32).min
                image = PIL.Image.fromarray(self.filled(fill).astype(np.float32), mode='F')
            # write image
            params.update({'tiffinfo': self._tags})
            image.save(fp=filename, **params)

        def asImage(self, scale='log', colormap='jet', inverse=False, linthresh=1.0, linscale=1.0):
            """
            Returns the sasImage as 8bit RGB image using PIL.

            See `PIL(Pillow) <https://pillow.readthedocs.io/en/latest/>`_ for more info about PIL images
            and image manipulation possibilities as e.g. in notes.
            Conversion to 8bit RGB looses floating point information but is for presenting and publication.

            Parameters
            ----------
            scale : 'log', 'symlog', default = 'norm'
                Scale for intensities.

                - 'norm' Linear scale.
                - 'log' Logarithmic scale
                - 'symlog' Symmetrical logarithmic scale is logarithmic in both the positive
                  and negative directions from the origin.
                  Use linthresh, linscale to adjust.
            colormap : string, None
                Colormap from matplotlib or None for grayscale.
                For standard colormap names look in js.mpl.showColors().
            inverse : bool
                Inverse colormap.
            linthresh : float, default = 1
                Only used for scale 'sym'.
                The range within which the plot is linear (-linthresh to linthresh).
            linscale : float, default = 1
                Only used for scale 'sym'.
                Its value is the number of decades to use for each half of the linear range.

            Returns
            -------
                PIL image

            Notes
            -----
            Pillow (fork of PIL)  allows image manipulation.
            As a prerequisite of Jscatter it is installed on your system and
            can be imported as ``import PIL`` ::

             image=mysasimage.asImage()
             image.show()                                             # show in system default viewer
             image.save('test.pdf', format=None, **params)            # save the image in different formats
             image.save('test.jpg',subsampling=0, quality=100)        # use these for best jpg quality
             image.save('test.png',transparency=(0,0,0))              # png image with black as transparent
             image.crop((10,10,200,200))                              # cut border

             import PIL.ImageOps as PIO
             nimage=PIO.equalize(image, mask=None)                    # Equalize the image histogram.
             nimage=PIO.autocontrast(image, cutoff=0, ignore=None)    # Automatic contrast
             nimage=PIO.expand(image, border=20, fill=(255,255,255))  # add border to image (here white)

            Examples
            --------
            ::

             import jscatter as js
             import PIL.ImageOps as PIO
             cal = js.sas.sasImage(js.examples.datapath+'/calibration.tiff')
             # create image for later usage
             image=cal.asImage(colormap='inferno',scale='log',inverse=1)
             # create image and just show it
             cal.asImage(colormap='inferno',scale='log').show()
             # expand image and show it or save it
             PIO.expand(image, border=20, fill=(255,255,255)).show()
             PIO.expand(image, border=20, fill=(255,255,255)).save('myimageas.pdf')

            """
            if colormap is not None:
                cmap = cm.get_cmap(colormap)
            else:
                cmap = cm.get_cmap('gray')

            if scale[:3] == 'log':
                norm = colors.LogNorm(clip=True)
            elif scale[:3] == 'sym':
                norm = colors.SymLogNorm(clip=True, linthresh=linthresh, linscale=linscale)
            else:  # default: scale == 'normalize':
                norm = colors.Normalize(clip=True)

            # initialize min,max values
            norm.autoscale(self)
            # do normalization
            data = norm(self)
            if inverse:
                data = 1 - data

            # conversion to colormap in range 0:255
            cdata = cmap(data, bytes=True)

            image = PIL.Image.fromarray(cdata[:, :, :-1], mode='RGB')

            return image

    def createImageFromArray(data, xgrid=None, zgrid=None, method='nearest', fill_value=0):
        """
        Create sasImage from 2D dataArray with .X and .Z coordinates and .Y values.

        If points are missing these are interpolated using .regrid.

        Parameters
        ----------
        data : dataArray
            Data to create image.
        xgrid : array, None, int
            New grid in x dimension. If None the unique values in .X are used.
            For integer the xgrid with these number of points between [min(X),max(X)] is generated.
        zgrid :array, None
            New grid in z dimension (second dimension). If None the unique values in .Z are used.
            For integer the zgrid with these number of points between [min(X),max(X)] is generated.
        method : float,'linear', 'nearest', 'cubic'
            Filling value for new points as float or order of interpolation
            between existing points.
            See `griddata <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.griddata.html>`_
        fill_value
            Value used to fill in for requested points outside of the convex
            hull of the input points.
            See `griddata <https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.griddata.html>`_

        Returns
        -------
            sasImage

        Examples
        --------
        ::

         import jscatter as js
         import numpy as np
         import matplotlib.pyplot as pyplot
         import matplotlib.tri as tri
         def func(x, y):
             return x*(1-x)*np.cos(4*np.pi*x) * np.sin(4*np.pi*y**2)**2

         # create random points in [0,1]
         xz = np.random.rand(1000, 2)
         v = func(xz[:,0], xz[:,1])
         # create dataArray
         data=js.dA(np.stack([xz[:,0], xz[:,1],v],axis=0),XYeYeX=[0, 2, None, None, 1, None])

         newdata=data.regrid(np.r_[0:1:100j],np.r_[0:1:200j],method='cubic')
         newdata.Y+=newdata.Y.max()
         image=js.sas.createImageFromArray(newdata,100,100)
         image.show()

        """
        ndata = data.regrid(xgrid=xgrid, zgrid=zgrid, wgrid=None, method=method, fill_value=fill_value)
        im = ndata.Y.reshape(np.unique(ndata.X).shape[0], np.unique(ndata.Z).shape[0])
        im2 = PIL.Image.fromarray(im)
        im2.tag = ''
        image = sasImage(im2)
        return image

    def readImages(filenames):
        """
        Read a list of images returning sasImage`s.

        Parameters
        ----------
        filenames : string
            Glob pattern to read

        Returns
        -------
            list of sasImage`s

        Notes
        -----
        To get a list of image descriptions::

         images=js.sas.readImages(path+'/latest*.tiff')
         [i.description for i in images]

        """
        try:
            filelist = glob.glob(filenames)
        except AttributeError:
            raise AttributeError('No filename pattern in ', filenames)
        else:
            data = []
            for ff in filelist:
                data.append(sasImage(ff))
        return data

    def createImageDescriptions(images):
        """
        Create text file with image descriptions as list of content.

        Parameters
        ----------
        images : list of sasImages or glob pattern
            List of images

        Returns
        -------

        """
        if not isinstance(images, (list, set)):
            images = readImages(images)
        commonprefix = os.path.commonprefix([i.filename for i in images])
        description = [i.filename[len(os.path.dirname(commonprefix)):] + '   ' + i.description for i in images]
        description.sort()
        commonname = os.path.split(commonprefix)[-1]
        if commonname == '':
            commonname = '--'
        with open('ContentOf_' + commonname + '.txt', 'w') as f:
            f.writelines("%s\n" % l for l in ['Content of dir ' + os.path.dirname(commonprefix), ' '])
            f.writelines("%s\n" % l for l in description)

    def createLogPNG(filenames, center=None, size=None, colormap='jet', equalize=False, contrast=None):
        """
        Create .png files from grayscale images with log scale conversion to values between [1,255].

        This generates images viewable in simple image viewers as overview.
        The new files are stored in the same folder as the original files.

        Parameters
        ----------
        filenames : string
            Filename with glob pattern as 'file*.tif'
        center : [int,int]
            Center of crop region.
        size : int
            Size of crop region.
             - If center is given a box with 2*size around center is used.
             - If center is None the border is cut by size.
        colormap : string, None
            Colormap from matplotlib or None for grayscale.
            For standard colormap names look in mpl.showColors().
        equalize : bool
            Equalize the images.
        contrast : None, float
            Autocontrast for the image.
            The value (0.1=10%) determines how much percent are cut from the intensity histogram before linear
            spread of intensities.

        """
        if colormap is not None:
            cmap = mpl.matplotlib.cm.get_cmap(colormap)
        else:
            cmap = None
        i1 = i3 = 0
        i2 = i4 = 100000
        if size is not None:
            # set box around center or from border
            if center is not None:
                i1 = center[1] - size
                i2 = center[1] + size
                i3 = center[0] - size
                i4 = center[0] + size
            else:
                i1 = size
                i2 = - size
                i3 = size
                i4 = - size
        try:
            filelist = glob.glob(filenames)
        except AttributeError:
            raise AttributeError('No filename pattern in ', filenames)
        else:
            for ff in filelist:
                image = PIL.Image.open(ff)
                # crop image array
                image2 = np.array(image)[max(i1, 0):min(i2, image.height), max(i3, 0):min(i4, image.width)]
                # log scale mapped to 0-255
                image2[image2 < 1] = 1
                image2 = np.log(image2)
                image2 = image2 / np.max(image2) * 255
                if cmap is None:
                    newimage = PIL.Image.fromarray(image2.astype(np.uint8)).convert('L')
                else:
                    # cmap needs uint to work properly
                    image2 = cmap(image2.astype(np.uint8), bytes=True)
                    newimage = PIL.Image.fromarray(image2[:, :, :-1], mode='RGB')
                if contrast is not None:
                    newimage = PIL.ImageOps.autocontrast(newimage, contrast)
                if equalize:
                    newimage = PIL.ImageOps.equalize(newimage)
                newimage.save(ff + '.png')
        return