From 834399eba2e25f58451f89c7c54af9faad94d047 Mon Sep 17 00:00:00 2001
From: Rafael Gort <rafael.gort@xfel.eu>
Date: Wed, 26 Aug 2020 15:44:51 +0200
Subject: [PATCH] Added DSSC related geometry methods and image manipulation
 routines. quick code structuring. not tested

---
 src/toolbox_scs/detectors/__init__.py         |   4 +-
 src/toolbox_scs/detectors/dssc.py             | 195 ++++++++--
 src/toolbox_scs/detectors/dssc_geometry.py    |  52 +++
 .../detectors/image_manipulation.py           | 336 ++++++++++++++++++
 4 files changed, 562 insertions(+), 25 deletions(-)
 create mode 100644 src/toolbox_scs/detectors/dssc_geometry.py
 create mode 100644 src/toolbox_scs/detectors/image_manipulation.py

diff --git a/src/toolbox_scs/detectors/__init__.py b/src/toolbox_scs/detectors/__init__.py
index 5059545..c2998c9 100644
--- a/src/toolbox_scs/detectors/__init__.py
+++ b/src/toolbox_scs/detectors/__init__.py
@@ -10,7 +10,7 @@ from .dssc_misc import (
 from .dssc_processing import (
     bin_data)
 from .dssc import (
-    DSSCBinner, DSSCFormatter, DSSCAnalyzer)
+    DSSCBinner, DSSCFormatter, DSSC_Generator)
 from .azimuthal_integrator import AzimuthalIntegrator
 
 __all__ = (
@@ -33,7 +33,7 @@ __all__ = (
     # Classes
     "DSSCBinner",
     "DSSCFormatter",
-    "DSSCAnalyzer",
+    "DSSC_Generator",
     "AzimuthalIntegrator",
     # Variables
 )
diff --git a/src/toolbox_scs/detectors/dssc.py b/src/toolbox_scs/detectors/dssc.py
index 189f7d7..9da1971 100644
--- a/src/toolbox_scs/detectors/dssc.py
+++ b/src/toolbox_scs/detectors/dssc.py
@@ -25,11 +25,13 @@ from .dssc_data import (
     save_xarray, load_xarray, save_attributes_h5,
     search_files, get_data_formatted)
 from .dssc_misc import (
-        load_dssc_info, get_xgm_formatted, load_geom)
+        load_dssc_info, get_xgm_formatted)
 from .dssc_processing import (
         bin_data, create_empty_dataset)
+from .image_manipulation import (
+        convert_hex2cart)
 
-__all__ = ["DSSCBinner", "DSSCFormatter", "DSSCAnalyzer"]
+__all__ = ["DSSCBinner", "DSSCFormatter", "DSSC_Generator"]
 log = logging.getLogger(__name__)
 
 
@@ -257,7 +259,7 @@ class DSSCFormatter:
         if os.path.exists(filepath):
             try:
                 self._filenames = search_files(filepath)
-            except ToolBoxFileError as err:
+            except ToolBoxFileError:
                 log.info("path did not contain any files")
         else:
             log.info("path did not exist")
@@ -338,30 +340,177 @@ class DSSCFormatter:
         save_attributes_h5(filename, self.attributes)
 
 
-class DSSCAnalyzer:
+class DSSC_Generator:
+
+    # geomtry = None
+    tile_geodat = np.zeros((16, 2, 2), dtype='int')
+    # definitions: [[voffset, hoffset], [is_flip, axis]]
+    # axis= 0, flip up-down
+    # axis= 1, flip left-right
+    # fractional shifts not included so far
+    tile_geodat[0, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[1, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[2, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[3, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[4, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[5, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[6, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[7, :, :] = [[0, 8], [1, 1]]
+    tile_geodat[8, :, :] = [[0, 8], [1, 0]]
+    tile_geodat[9, :, :] = [[0, 8], [1, 0]]
+    tile_geodat[10, :, :] = [[0, 8], [1, 0]]
+    tile_geodat[11, :, :] = [[0, 8], [1, 0]]
+    tile_geodat[12, :, :] = [[0, 8], [1, 0]]
+    tile_geodat[13, :, :] = [[0, 8], [1, 0]]
+    tile_geodat[14, :, :] = [[0, 8], [1, 0]]
+    tile_geodat[15, :, :] = [[0, 8], [1, 0]]
+
+    module_geodat = np.zeros((16, 2), dtype='int')
+    module_geodat[0, :] = [0, 0]
+    module_geodat[1, :] = [19, 0]
+    module_geodat[2, :] = [18, 0]
+    module_geodat[3, :] = [19, 0]
+
+    module_geodat[4, :] = [0, 0]
+    module_geodat[5, :] = [19, 0]
+    module_geodat[6, :] = [19, 0]
+    module_geodat[7, :] = [18, 0]
+
+    module_geodat[11, :] = [0, 0]
+    module_geodat[10, :] = [19, 0]
+    module_geodat[9, :] = [18, 0]
+    module_geodat[8, :] = [19, 0]
+
+    module_geodat[15, :] = [0, 0]
+    module_geodat[14, :] = [19, 0]
+    module_geodat[13, :] = [19, 0]
+    module_geodat[12, :] = [18, 0]
+
+    quad_geodat = np.zeros((4, 2), dtype='int')
+    quad_geodat[0, :] = [10, 17]  # [quadrant 1, insert, gap]
+    quad_geodat[1, :] = [8, 26]  # [quadrant 2, insert, gap]
+    quad_geodat[2, :] = [-9, 19]  # [quadrant 3, insert, gap]
+    quad_geodat[3, :] = [-9, 25]  # [quadrant 4, insert, gap]
+
     def __init__(self):
+        pass
+
+    def read_geometry(self, filename):
+        pass
+
+    def save_geometry(self, filename):
+        pass
+
+    def full_detector(self, dssc_data):
+        print('assemble full detector: ')
+        # determine dimension of full detector
+        [qdimy, qdimx] = q1.shape
+        fdimy = np.max(
+                    [np.abs(self.quad_geodat[1, 0]) + self.quad_geodat[1, 1],
+                     np.abs(self.quad_geodat[3, 0]) + self.quad_geodat[3, 1]]
+                    ) + 2 * qdimy
+        fdimx = np.max(
+                    [np.abs(self.quad_geodat[2, 0]) + self.quad_geodat[2, 1],
+                     np.abs(self.quad_geodat[0, 0]) + self.quad_geodat[0, 1]]
+                    ) + 2 * qdimx
+
+        # assemble full detector
+        full_dssc = np.zeros((fdimy, fdimx))
+        full_dssc[self.quad_geodat[1, 0]:qdimy+self.quad_geodat[1, 0], 0:qdimx ] \
+            = self.quadrant(2, dssc_data)
+        full_dssc[qdimy+self.quad_geodat[1, 0]+self.quad_geodat[1, 1]:2*qdimy+self.quad_geodat[1, 0]+self.quad_geodat[1, 1],
+                  self.quad_geodat[0, 0]:self.quad_geodat[0, 0]+qdimx] \
+            = self.quadrant(1, dssc_data)#
+
+        dummy1 = [qdimy + self.quad_geodat[1, 0] + self.quad_geodat[1, 1] + self.quad_geodat[3, 0],
+                2 * qdimy + self.quad_geodat[1, 0] + self.quad_geodat[1, 1] + self.quad_geodat[3, 0]]
+        dummy2 = [self.quad_geodat[0, 0] + self.quad_geodat[0, 1] + qdimx,
+                self.quad_geodat[0, 0] + self.quad_geodat[0, 1] + 2*qdimx]
+        full_dssc[dummy1[0]:dummy1[1], dummy2[0]:dummy2[1]] = self.quadrant(4, dssc_data)
+
+        dummy3 = [self.quad_geodat[1, 0] + self.quad_geodat[1, 1] + self.quad_geodat[3, 0] - self.quad_geodat[3, 1],
+                qdimy+self.quad_geodat[1, 0] + self.quad_geodat[1, 1] + self.quad_geodat[3, 0] - self.quad_geodat[3, 1]]
+        dummy4 = [self.quad_geodat[0, 0] + self.quad_geodat[0, 1] + qdimx + self.quad_geodat[2, 0],
+                self.quad_geodat[0, 0] + self.quad_geodat[0, 1] + 2*qdimx + self.quad_geodat[2, 0]]
+
+        full_dssc[dummy3[0]:dummy3[1],dummy4[0]:dummy4[1]] = self.quadrant(3, dssc_data)
+
+        return full_dssc
+
+    def quadrant(self, q, dssc_data):
+        """
+        q: quadrant index 0-3
+        dssc_data: (15, 128, 512)
+        """
+        print("collecting quadrant: Q" + str(q))
+        if q == 1:
+            module0 = self.convert_module(0, dssc_data[0, :, :])
+            mdimx = module0.shape[1]
+            qu = [module0]
+            for n in range(1, 4):
+                qu.append(np.zeros((self.module_geodat[n, 0], mdimx), dtype='float32'))
+                qu.append(self.convert_module(n, dssc_data[n, :, :]))
+        elif q == 2:
+            module0 = self.convert_module(4, dssc_data[4, :, :])
+            mdimx = module0.shape[1]
+            qu = [module0]
+            for n in range(5, 8):
+                qu.append(np.zeros((self.module_geodat[n, 0], mdimx), dtype='float32'))
+                qu.append(self.convert_module(n, dssc_data[n, :, :]))
+        elif q == 3:
+            module0 = self.convert_module(11, dssc_data[11, :, :])
+            mdimx = module0.shape[1]
+            qu = [module0]
+            for n in range(10, 7, -1):
+                qu.append(np.zeros((self.module_geodat[n, 0], mdimx), dtype='float32'))
+                qu.append(self.convert_module(n, dssc_data[n, :, :]))
+        elif q == 4:
+            module0 = self.convert_module(15, dssc_data[15, :, :])
+            mdimx = module0.shape[1]
+            qu = [module0]
+            for n in range(14, 11, -1):
+                qu.append(np.zeros((self.module_geodat[n, 0], mdimx), dtype='float32'))
+                qu.append(self.convert_module(n, dssc_data[n, :, :]))
+
+        return np.vstack(qu)
+
+    def convert_module(self, m, module_hex):
         """
-        Placeholder for future class handling image operation/manipulation
-        (the actual data evaluation).
+        convert hex to cartesian
+        add gap according to geometry file
+        flip according to geometry file
+        returns the module in cartesian coordinates with the correct gap between tiles
+        """
+        print("processing module: " + str(m))
+        tile_hex = self.split_module(module_hex)
+        offset, flip_module = self._get_tile_geodat(m)
 
-        Parameters
-        ----------
-        ToDo ...
+        module_cart = np.zeros((120 + offset[0], 2*276 + offset[1]), dtype='float32')
+        module_cart[0:120, 0:276] = convert_hex2cart(tile_hex[0, :, :])
+        module_cart[0:120, 276 + offset[1]:552 + offset[1]] = convert_hex2cart(tile_hex[1, :, :])
 
-        Returns
-        -------
-        ToDo ...
+        if flip_module[0]:
+            module_cart = np.flip(module_cart, axis=flip_module[1])
+            #print('flipped module: ' + str(flip_module[1]))
 
-        Raises
-        ------
-        ToDo....
+        return module_cart
 
-        Example
-        -------
-        """
-        # self.distance = distance
-        # self.pxpitchh = 236
-        # self.pxpitchv = 204
-        # self.aspect = self.pxpitchv/self.pxpitchh
-        # .....
+    def split_module(self, module_hex):
+        # return two tiles
+        tile_hex = np.zeros((2, 128, 256), dtype='float32')
+        tile_hex[0, :, :] = module_hex[:, 0:256]
+        tile_hex[1, :, :] = module_hex[:, 256:512]
+        return tile_hex
+
+    def _get_tile_geodat(self, m):
+        return self.tile_geodat[m, 0, :], self.tile_geodat[m, 1, :]
+
+    def _default_geometry(self):
         pass
+
+# for all modules do
+# 1) fix pedestals by ASIC mask and add/subtract pedestal offset
+# 2) split module into its tiles
+# 3) convert tiles from hex to cart
+# 4) reposition tiles according to offsets (from geometry file)
+# 5) figure out orientation and flip up-down or left-right (flag in geometry file)
diff --git a/src/toolbox_scs/detectors/dssc_geometry.py b/src/toolbox_scs/detectors/dssc_geometry.py
new file mode 100644
index 0000000..af14223
--- /dev/null
+++ b/src/toolbox_scs/detectors/dssc_geometry.py
@@ -0,0 +1,52 @@
+"""
+    DSSC-geometry related sub-routines.
+
+    comment: contributions should comply with pep8 code structure guidelines.
+"""
+import logging
+
+import numpy as np
+import skimage.util as sku
+import skimage.transform as skt
+
+log = logging.getLogger(__name__)
+
+
+def slice_module(module):
+    tile = np.zeros((2, 128, 256), dtype='float32')
+    tile[0, :, :] = module[:, 0:256]
+    tile[1, :, :] = module[:, 256:512]
+    print(tile.shape)
+    return tile
+
+
+def data_from_symmetry(data, center=[584.0, 544.0], threshold=0.0):
+    """
+    Parameters
+    ----------
+    data: np.ndarray
+        2-dimensional numpy-array containing the raw image data.
+    center: list
+        a list with two values indicating the physical center, i.e. the
+        position of the beam.
+    threshold: float
+        The lower magnitude limit, below which data from the image should
+        be replaced with data from the rotated image.
+
+    Returns
+    -------
+    data_rotated: np.ndarray
+        A 2-dimensional numpy-array containing data from the rotated image,
+        where it is missing in the original one.
+    """
+    image = np.nan_to_num(sku.img_as_float(data))
+    shape = np.shape(image)
+
+    shifts = [(shape[i] - 2*(shape[i]/2.0 - center[i])) for i in range(2)]
+
+    tform = skt.EuclideanTransform(rotation=np.pi, translation=shifts[::-1])
+    data_rotated = skt.warp(image, tform)
+
+    mask = (image <= threshold)
+    data_rotated[~mask] = 0.0
+    return data_rotated
diff --git a/src/toolbox_scs/detectors/image_manipulation.py b/src/toolbox_scs/detectors/image_manipulation.py
new file mode 100644
index 0000000..e4ecc9a
--- /dev/null
+++ b/src/toolbox_scs/detectors/image_manipulation.py
@@ -0,0 +1,336 @@
+"""
+Collection of Filters for image processing:
+Thiran filters for delay shifters. The filter is used for image shearing.
+"""
+import logging
+
+import numpy as np
+import math as math
+from skimage.util import pad as skpad
+from skimage import data as skdata
+
+log = logging.getLogger(__name__)
+
+
+def n_choose_k(n, k):
+    """
+    :n choose k problem: returns the number of ways to choose a subset k from a
+    set of n elements this is eqivalent to:
+        factorial(n)/factorial(k)/factorial(n-k)
+
+    written by A. Scherz
+    """
+    return math.factorial(n) / math.factorial(k) / math.factorial(n - k)
+
+
+def thiran_type_I(order=3):
+    return Thiran(1, order)
+
+
+def thiran_type_II(order=2):
+    return Thiran(2, order)
+
+
+class Thiran:
+    """
+    there are two types of 1D fractional filters based on Thiran filters. both
+    types can have an order N=1 to inf. some types, provide simple algorithms
+    for applying the delay to a signal. for type II and orders larger than N.
+    he transfer and impulse response fct have to be calculated and are more
+    computational expensive
+    The generator defaults the Thiran filter of type I and order N=3
+    For more details see of the filters and algorithms,
+        see pg. 685, IEEE Trans. on Im. Proc. 17, 679 (2008)
+
+    written by A. Scherz, XFEL, 2012-11-26
+    updated by A. Scherz, XFEL, 2019-02-11 for python
+    added to SpeckleToolbox by A. Scherz, XFEL, 2020-08-06
+    """
+
+    type_I = staticmethod(thiran_type_I)
+
+    type_II = staticmethod(thiran_type_II)
+
+    def __init__(self, filter_type=1, order=3, delay=0):
+        self._type = filter_type
+        self._n = order
+        self._delay = delay
+        self._b = np.zeros(order)
+        self._a_minus_delay = 0
+        self._a_plus_delay = 0
+
+    def __str__(self):
+        print('Thiran 1D Fractional filter')
+        print('  Type : ' + self._type.__str__())
+        print(' Order : ' + self._n.__str__())
+        print(' Delay : ' + self._delay.__str__())
+
+        return ''
+
+    def apply_delay(self, signal):
+        # periodic boundaries are important. add padding of at least the order
+        # N left and right
+        # this should be applied before calling this method for type I
+        shifted_signal = np.array(signal)
+
+        if self._type == 1:     # type I Thiran
+            for j in range(len(shifted_signal) - self._n - 1, self._n - 1, -1):
+                for k in range(1, self._n + 1):
+                    shifted_signal[j] += self._b[k - 1] \
+                        * (shifted_signal[j - k] - shifted_signal[j + k])
+        else:   # type II of order N = 2
+            for j in range(self._n - 1, len(shifted_signal) - self._n - 1, 1):
+                shifted_signal[j] += self._a_plus_delay \
+                    * (shifted_signal[j - 1] - shifted_signal[j + 1])
+            for j in range(len(shifted_signal) - self._n - 1, self._n - 1, -1):
+                shifted_signal[j] += self._a_minus_delay \
+                    * (shifted_signal[j + 1] - shifted_signal[j - 1])
+
+        return shifted_signal  # shifted signal
+
+    def delay(self, tau):
+        if (tau < -0.5) or (tau > 0.5):
+            raise ValueError('WARNING out of range: delay: 0.0 - 0.5')
+        self._delay = tau
+        # update coefficients for delay
+        self._coefficients()
+        return self
+
+    def _coefficients(self):
+        for k in range(1, self._n + 1):
+            p_k = 1
+            for l in range(self._n + 1):
+                p_k *= (self._delay - l) / (self._delay - l - k)
+            # n_choose_k function is factorial(n)/factorial(k)/factorial(n-k)
+            self._b[k - 1] = (-1) ** k * p_k * n_choose_k(self._n, k)
+        self._a_plus_delay = \
+            (-4 + self._delay**2 + np.sqrt(12 - 3*self._delay**2))
+        self._a_minus_delay = self._a_plus_delay
+        self._a_plus_delay /= (3*self._delay + 2 + self._delay**2)
+        self._a_minus_delay /= (-3*self._delay + 2 + self._delay**2)
+
+
+def is_even(num):
+    """
+    is num an even number?
+    :param num: integer
+    :return: boolean True = num is even number
+    """
+    return not (num & 1)
+
+
+def next_power_of_2(x=1):
+    """
+    Returns the next higher power of 2 compared to x
+    :param x: current dimension
+    :return: next higher power of 2
+    """
+    return 1 << (x - 1).bit_length()
+
+
+def get_im_center(im: np.array) -> (int, int):
+    """
+    the center of image is the DC components with respect to discrete Fourier
+    transformation
+    :returns center pos (int):
+    """
+    return int(im.shape[0] / 2), int(im.shape[1] / 2)
+
+
+def pad4fft(im: np.array = None,
+            dim: [None, bool, int] = None,
+            fill: [None, str, float] = None) -> np.array:
+    """
+    pad image using dim parameter and pad image with given fill
+    :param im:
+    :param dim: None == up to next power of 2, True/False == oversampling, or
+    int with given dimension
+    :param fill: None == zeros, str == special format see skimage.pad, float ==
+    fill value
+    :return: object itself
+    """
+    if im is not None:
+        im = np.array(im).squeeze()
+    else:
+        im = skdata.lena()
+    #print('image size is ' + str(im.shape))
+    #print('dimension is supposed to be ' + str(dim))
+
+    if dim is None:
+        dim = max((next_power_of_2(im.shape[0]), next_power_of_2(im.shape[1])))
+        dim = (dim, dim)
+    elif type(dim) is bool:
+        oversampling = dim
+        print('is boolean option')
+        dim = max((next_power_of_2(im.shape[0]), next_power_of_2(im.shape[1])))
+        dim *= 2 if oversampling else 1  # another factor 2 for oversampling
+        dim = (dim, dim)
+    elif (dim[0] < im.shape[0]) and (dim[1] < im.shape[1]):
+        return im
+
+    #print('dimension is ' + str(dim))
+
+    w0 = ((dim[0] - im.shape[0]) // 2)
+    w1 = ((dim[1] - im.shape[1]) // 2)
+
+    width0 = (w0, w0) if is_even(im.shape[0]) else (w0, w0 + 1)
+    width1 = (w1, w1) if is_even(im.shape[1]) else (w1, w1 + 1)
+
+    if fill is None:
+        im = skpad(im, (width0, width1), mode='constant', constant_values=0)
+    elif type(fill) is str:
+        im = skpad(im, (width0, width1), mode=fill)
+    else:
+        im = skpad(im, (width0, width1), mode='constant', constant_values=fill)
+
+    return im
+
+
+def shear_im(im: np.array, delay: float,
+             im_axis: int = 1,
+             thiran: Thiran = thiran_type_I()) -> np.array:
+    """
+    : direction (str): 1=='hor' or 0=='ver', required
+    : thiran: Thiran type 1 of order 3 is default (default)
+    """
+    im = np.float32(im)
+    cx, cy = get_im_center(im)
+    # print(im.shape)
+    # print(cx, cy)
+    if im_axis == 0:
+        for k in range(2 * cy):
+            shift = delay * (k - cy)
+            # full pixel shifts
+            im[:, k] = np.roll(im[:, k], round(shift))
+            # now shift a pixel fraction
+            im[:, k] = thiran.delay(shift-round(shift)).apply_delay(im[:, k])
+            #  thiran_filter(im[:, k], thiran(order, shift - round(shift)))
+        # print('apply vertical shear')
+    elif im_axis == 1:
+        for k in range(2 * cx):
+            shift = delay * (k - cx)
+            # full pixel shifts
+            im[k, :] = np.roll(im[k, :], round(shift))
+            # now shift a pixel fraction: shift between +/- [0-0.5]
+            im[k, :] = thiran.delay(shift - round(shift)).apply_delay(im[k, :])
+            # thiran_filter(im[k, :], thiran(order, shift - round(shift)))
+        #print('apply horizontal shear')
+    else:
+        # TODO: throw exception
+        print('direction undefined')
+
+    return im
+
+
+def convert_cart2hex(im, thiran=thiran_type_II()):
+    """
+    [ imH ] = ConversionCart2Hex( imC, orderN, showImage )
+    imC: image in cartesian coord.
+    thiran: Thiran filter type 1 of order 3 is default
+    show: optional, if true an image representation in hex is shown.
+    note: show hex image may take considerable amount of time for larger
+    images!)
+    Nov.28, 2012, A. Scherz
+        """
+    # first step: zero pad the image for extending boundaries
+
+    im_hex = pad4fft(im, dim=[2*im.shape[0], 2*im.shape[1]], fill='reflect')
+    cx, cy = get_im_center(im_hex)
+
+    sqrt3 = math.sqrt(3)
+    shear1 = sqrt3 - math.sqrt(6 / sqrt3)
+    shear2 = math.sqrt(sqrt3 / 6)
+    shear3 = 2 - math.sqrt(6 / sqrt3)
+    # this is conversion from cartesian to hexagonal lattice
+    im_hex = shear_im(im_hex, -shear1, im_axis=0, thiran=thiran)
+    im_hex = shear_im(im_hex, -shear2, im_axis=1, thiran=thiran)
+    im_hex = shear_im(im_hex, -shear3, im_axis=0, thiran=thiran)
+
+    # reorder of pixels required
+    height = int(np.round(cart2hex([im.shape[0], 0]))[0])
+    width = int(np.round(cart2hex([0, im.shape[1]]))[1])
+    y0 = height % 2
+
+    # print(height, width, y0)
+    for y in range(-height // 2, height // 2 + y0 + 1):
+        im_hex[y - y0 + cx, :] = np.roll(im_hex[y - y0 + cx, :],
+                                         math.floor((y - y0) / 2))
+
+    # now crop image
+    im_hex = im_hex[-height // 2 + cx: height // 2 + cx + y0,
+                    -width // 2 + cy: width // 2 + cy + 1]
+
+    # TODO check height and width
+    # implement show
+
+    return im_hex
+
+
+def convert_hex2cart(im_hex, thiran=thiran_type_II()):
+    """
+    [ imH ] = ConversionCart2Hex( imC, orderN, showImage )
+    imC: image in cartesian coord.
+    thiran: Thiran filter type 1 of order 3 is default
+    show: optional, if true an image representation in hex is shown.
+    note: show hex image may take considerable amount of time for larger
+    images!)
+    Nov.28, 2012, A. Scherz
+    """
+    # first step: zero pad the image for extending boundaries
+
+    im = pad4fft(im_hex,
+                 dim=[2*im_hex.shape[0], 2*im_hex.shape[1]],
+                 fill='reflect')
+    cx, cy = get_im_center(im)
+
+    # need to skew image to hexagonal shape before apply shear operations
+    height = im_hex.shape[0]
+    y0 = height % 2
+    for y in range(- height // 2, height // 2 + y0 + 1):
+        im[y - y0 + cx, :] = np.roll(im[y - y0 + cx, :],
+                                     - math.floor((y + y0) / 2))
+
+    sqrt3 = math.sqrt(3)
+    shear1 = sqrt3 - math.sqrt(6 / sqrt3)
+    shear2 = math.sqrt(sqrt3 / 6)
+    shear3 = 2 - math.sqrt(6 / sqrt3)
+    # this is conversion from cartesian to hexagonal lattice
+    im = shear_im(im, shear3, im_axis=0, thiran=thiran)
+    im = shear_im(im, shear2, im_axis=1, thiran=thiran)
+    im = shear_im(im, shear1, im_axis=0, thiran=thiran)
+
+    # the image should now form a rectangular box of following size:
+    height = int(np.round(hex2cart([im_hex.shape[0], 0]))[0])
+    width = int(np.round(hex2cart([0, im_hex.shape[1]]))[1])
+    y0 = height % 2
+
+#    print(height, width, y0)
+#    for y in range(-height // 2, height // 2 + y0 + 1):
+#        im_hex[y - y0 + cx, :] = np.roll(im_hex[y - y0 + cx, :],
+#                                         math.floor((y - y0) / 2))
+
+    # now crop image
+    im = im[-height // 2 + cx: height // 2 + cx + y0,
+            -width // 2 + cy: width // 2 + cy + 1]
+
+    # TODO check height and width
+    # implement show
+
+    return im
+
+
+def cart2hex(r_cart):
+    # TODO: this function can eventually move somewhere else
+    sqrt3 = math.sqrt(3)
+    # conversion matrix
+    rhex = math.sqrt(2 / sqrt3) * np.array([[1, 1 / 2], [0, sqrt3 / 2]])
+
+    return np.dot(rhex, r_cart)
+
+
+def hex2cart(r_hex):
+    sqrt3 = math.sqrt(3)
+    rhex = math.sqrt(2 / sqrt3) * np.array([[1, 1 / 2], [0, sqrt3 / 2]])
+    rcart = np.linalg.inv(rhex)
+
+    return np.dot(rcart, r_hex)
-- 
GitLab