Exemple #1
0
    def distance(self, threads=2):
        """ @brief Finds the distance transform of an input image.
            @param threads Number of threads to use
            @returns A one-channel floating-point image
        """
        input = self.copy(depth=8)

        # Temporary storage for G lattice
        g = self.__class__(
            self.width, self.height,
            channels=1, depth=32
        )
        ibounds = [int(t/float(threads)*self.width) for t in range(threads)]
        ibounds = zip(ibounds, ibounds[1:] + [self.width])

        args1 = [(i[0], i[1], self.width, self.height, input.pixels, g.pixels)
                 for i in ibounds]

        multithread(libfab.distance_transform1, args1)

        del input

        output = self.copy(depth='f')

        jbounds = [int(t/float(threads)*self.height) for t in range(threads)]
        jbounds = zip(jbounds, jbounds[1:] + [self.height])

        args2 = [(j[0], j[1], self.width, self.pixels_per_mm,
                 g.pixels, output.pixels) for j in jbounds]

        multithread(libfab.distance_transform2, args2)

        output.zmin = output.zmax = None

        return output
Exemple #2
0
    def triangulate(self, threads=True, interrupt=None):
        """ @brief Triangulates an ASDF, returning a mesh
            @param threads Boolean determining multithreading
            @param interrupt threading.Event used to abort
            @returns A Mesh containing the triangulated ASDF
        """
        # Create an event to interrupt the evaluation
        if interrupt is None:   interrupt = threading.Event()

        # Shared flag to interrupt rendering
        halt = ctypes.c_int(0)

        # Create a set of arguments
        if threads:
            q = Queue.Queue()
            args = []
            for b in self.branches:
                if b is None:   continue
                args.append( (b, halt, q) )

            # Run the triangulation operation in parallel
            multithread(ASDF._triangulate, args, interrupt, halt)

            results = []
            while True:
                try:                results.append(q.get_nowait())
                except Queue.Empty: break
        else:
            results = [self._triangulate(halt)]
        m = Mesh.merge(results)
        m.color = self.color
        return m
Exemple #3
0
    def triangulate(self, threads=True, interrupt=None):
        """ @brief Triangulates an ASDF, returning a mesh
            @param threads Boolean determining multithreading
            @param interrupt threading.Event used to abort
            @returns A Mesh containing the triangulated ASDF
        """
        # Create an event to interrupt the evaluation
        if interrupt is None: interrupt = threading.Event()

        # Shared flag to interrupt rendering
        halt = ctypes.c_int(0)

        # Create a set of arguments
        if threads:
            q = Queue.Queue()
            args = []
            for b in self.branches:
                if b is None: continue
                args.append((b, halt, q))

            # Run the triangulation operation in parallel
            multithread(ASDF._triangulate, args, interrupt, halt)

            results = []
            while True:
                try:
                    results.append(q.get_nowait())
                except Queue.Empty:
                    break
        else:
            results = [self._triangulate(halt)]
        m = Mesh.merge(results)
        m.color = self.color
        return m
Exemple #4
0
    def render_multi(self,
                     region=None,
                     threads=8,
                     alpha=0,
                     beta=0,
                     resolution=10):
        """ @brief Renders to an image
            @param region Render region (default bounding box)
            @param threads Threads to use (default 8)
            @param alpha Rotation about Z axis (default 0)
            @param beta Rotation about X axis (default 0)
            @resolution Resolution in voxels per mm
            @returns A tuple with a height-map, shaded image, and image with colored normals
        """
        if region is None:
            region = self.bounding_region(resolution, alpha, beta)

        depth = Image(
            region.ni,
            region.nj,
            channels=1,
            depth=16,
        )
        shaded = Image(
            region.ni,
            region.nj,
            channels=1,
            depth=16,
        )
        normals = Image(
            region.ni,
            region.nj,
            channels=3,
            depth=8,
        )

        subregions = region.split_xy(threads)

        M = (ctypes.c_float * 4)(cos(radians(alpha)), sin(radians(alpha)),
                                 cos(radians(beta)), sin(radians(beta)))
        args = [(self.ptr, s, M, depth.pixels, shaded.pixels, normals.pixels)
                for s in subregions]
        multithread(libfab.render_asdf_shaded, args)

        for image in [depth, shaded, normals]:
            image.xmin = region.X[0]
            image.xmax = region.X[region.ni]
            image.ymin = region.Y[0]
            image.ymax = region.Y[region.nj]
            image.zmin = region.Z[0]
            image.zmax = region.Z[region.nk]
        return depth, shaded, normals
Exemple #5
0
    def render_multi(self, region=None, threads=8,
                     alpha=0, beta=0, resolution=10):
        """ @brief Renders to an image
            @param region Render region (default bounding box)
            @param threads Threads to use (default 8)
            @param alpha Rotation about Z axis (default 0)
            @param beta Rotation about X axis (default 0)
            @resolution Resolution in voxels per mm
            @returns A tuple with a height-map, shaded image, and image with colored normals
        """
        if region is None:
            region = self.bounding_region(resolution, alpha, beta)

        depth   = Image(
            region.ni, region.nj, channels=1, depth=16,
        )
        shaded  = Image(
            region.ni, region.nj, channels=1, depth=16,
        )
        normals = Image(
            region.ni, region.nj, channels=3, depth=8,
        )

        subregions = region.split_xy(threads)

        M = (ctypes.c_float*4)(cos(radians(alpha)), sin(radians(alpha)),
                               cos(radians(beta)), sin(radians(beta)))
        args = [
            (self.ptr, s, M, depth.pixels, shaded.pixels, normals.pixels)
            for s in subregions
        ]
        multithread(libfab.render_asdf_shaded, args)

        for image in [depth, shaded, normals]:
            image.xmin = region.X[0]
            image.xmax = region.X[region.ni]
            image.ymin = region.Y[0]
            image.ymax = region.Y[region.nj]
            image.zmin = region.Z[0]
            image.zmax = region.Z[region.nk]
        return depth, shaded, normals
    def distance(self, threads=2):
        """ @brief Finds the distance transform of an input image.
            @param threads Number of threads to use
            @returns A one-channel floating-point image
        """
        input = self.copy(depth=8)
         

        # Temporary storage for G lattice
        g = self.__class__(
                self.width, self.height, self.scale,
            channels=1, depth=32
        )
        ibounds = [int(t/float(threads)*self.width) for t in range(threads)]
        ibounds = zip(ibounds, ibounds[1:] + [self.width])

        args1 = [(i[0], i[1], self.width, self.height, input.pixels, g.pixels)
                 for i in ibounds]
        
        multithread(libfab.distance_transform1, args1)

        del input

        output = self.copy(depth='f')

        jbounds = [int(t/float(threads)*self.height) for t in range(threads)]
        jbounds = zip(jbounds, jbounds[1:] + [self.height])

        args2 = [(j[0], j[1], self.width, self.pixels_per_mm,
                 g.pixels, output.pixels) for j in jbounds]

        multithread(libfab.distance_transform2, args2)

        output.zmin = output.zmax = None

        return output
Exemple #7
0
    def asdf(self, region=None, resolution=None, mm_per_unit=None,
             merge_leafs=True, interrupt=None):
        """ @brief Constructs an ASDF from a math tree.
            @details Runs in up to eight threads.
            @param region Evaluation region (if None, taken from expression bounds)
            @param resolution Render resolution in voxels/unit
            @param mm_per_unit Real-world scale
            @param merge_leafs Boolean determining whether leaf cells are combined
            @param interrupt threading.Event that aborts rendering if set
            @returns ASDF data structure
        """

        if region is None:
            if not self.bounded:
                raise Exception('Unknown render region!')
            elif resolution is None:
                raise Exception('Region or resolution must be provided!')
            region = Region(
                (self.xmin, self.ymin, self.zmin if self.zmin else 0),
                (self.xmax, self.ymax, self.zmax if self.zmax else 0),
                resolution
            )

        if interrupt is None:   interrupt = threading.Event()

        # Shared flag to interrupt rendering
        halt = ctypes.c_int(0)

        # Split the region into up to 8 sections
        split = region.octsect(all=True)
        subregions = [split[i] for i in range(8) if split[i] is not None]
        ids = [i for i in range(8) if split[i] is not None]

        threads = len(subregions)
        clones  = [self.clone() for i in range(threads)]
        packed  = [libfab.make_packed(c.ptr) for c in clones]

        # Generate a root for the tree
        asdf = ASDF(libfab.asdf_root(packed[0], region), color=self.color)

        # Multithread the solver process
        q = Queue.Queue()
        args = zip(packed, ids, subregions, [q]*threads)

        # Helper function to construct a single branch
        def construct_branch(ptree, id, region, queue):
            asdf = libfab.build_asdf(ptree, region, merge_leafs, halt)
            queue.put((id, asdf))

        # Run the constructor in parallel to make the branches
        multithread(construct_branch, args, interrupt, halt)
        for p in packed:    libfab.free_packed(p)

        # Attach the branches to the root
        for s in subregions:
            try:                id, branch = q.get_nowait()
            except Queue.Empty: break
            else:               asdf.ptr.contents.branches[id] = branch
        libfab.get_d_from_children(asdf.ptr)
        libfab.simplify(asdf.ptr, merge_leafs)

        # Set a scale on the ASDF if one was provided
        if mm_per_unit is not None:     asdf.rescale(mm_per_unit)

        return asdf
Exemple #8
0
    def asdf(self,
             region=None,
             resolution=None,
             mm_per_unit=None,
             merge_leafs=True,
             interrupt=None):
        """ @brief Constructs an ASDF from a math tree.
            @details Runs in up to eight threads.
            @param region Evaluation region (if None, taken from expression bounds)
            @param resolution Render resolution in voxels/unit
            @param mm_per_unit Real-world scale
            @param merge_leafs Boolean determining whether leaf cells are combined
            @param interrupt threading.Event that aborts rendering if set
            @returns ASDF data structure
        """

        if region is None:
            if not self.bounded:
                raise Exception('Unknown render region!')
            elif resolution is None:
                raise Exception('Region or resolution must be provided!')
            region = Region(
                (self.xmin, self.ymin, self.zmin if self.zmin else 0),
                (self.xmax, self.ymax, self.zmax if self.zmax else 0),
                resolution)

        if interrupt is None: interrupt = threading.Event()

        # Shared flag to interrupt rendering
        halt = ctypes.c_int(0)

        # Split the region into up to 8 sections
        split = region.octsect(all=True)
        subregions = [split[i] for i in range(8) if split[i] is not None]
        ids = [i for i in range(8) if split[i] is not None]

        threads = len(subregions)
        clones = [self.clone() for i in range(threads)]
        packed = [libfab.make_packed(c.ptr) for c in clones]

        # Generate a root for the tree
        asdf = ASDF(libfab.asdf_root(packed[0], region), color=self.color)
        asdf.lock.acquire()

        # Multithread the solver process
        q = Queue.Queue()
        args = zip(packed, ids, subregions, [q] * threads)

        # Helper function to construct a single branch
        def construct_branch(ptree, id, region, queue):
            asdf = libfab.build_asdf(ptree, region, merge_leafs, halt)
            queue.put((id, asdf))

        # Run the constructor in parallel to make the branches
        multithread(construct_branch, args, interrupt, halt)
        for p in packed:
            libfab.free_packed(p)

        # Attach the branches to the root
        for s in subregions:
            try:
                id, branch = q.get_nowait()
            except Queue.Empty:
                break
            else:
                # Make sure we didn't get a NULL pointer back
                # (which could occur if the halt flag was raised)
                try:
                    branch.contents
                except ValueError:
                    asdf.lock.release()
                    return None
                asdf.ptr.contents.branches[id] = branch
        libfab.get_d_from_children(asdf.ptr)
        libfab.simplify(asdf.ptr, merge_leafs)
        asdf.lock.release()

        # Set a scale on the ASDF if one was provided
        if mm_per_unit is not None: asdf.rescale(mm_per_unit)

        return asdf
Exemple #9
0
class MathTree(object):
    """ @class MathTree
        @brief Represents a distance metric math expression.

        @details
        Arithmetic operators are overloaded to extend the tree with
        either distance metric arithmetic or shape logical expressions,
        depending on the value of the instance variable 'shape'
    """
    def __init__(self, math, shape=False, color=None):
        """ @brief MathTree constructor
            @param math Math string (in prefix notation)
            @param shape Boolean modifying arithmetic operators
            @param color Color tuple or None
        """

        ## @var math
        # Math string (in sparse prefix syntax)
        if type(math) in [int, float]:
            self.math = 'f' + str(math)
        else:
            self.math = math

        ## @var shape
        # Boolean modify the behavior of arithmetic operators
        self.shape = shape

        ## @var color
        # Assigned color, or None
        self.color = color

        self._str = None
        self._ptr = None

        ## @var bounds
        # X, Y, Z bounds (or None)
        self.bounds = [None] * 6

        self.lock = threading.Lock()

    @threadsafe
    def __del__(self):
        """ @brief MathTree destructor """
        if self._ptr is not None and libfab is not None:
            libfab.free_tree(self.ptr)

    @property
    def ptr(self):
        """ @brief Parses self.math and returns a pointer to a MathTree structure
        """
        if self._ptr is None:
            self._ptr = libfab.parse(self.math)
        return self._ptr

    ############################################################################

    @property
    def dx(self):
        try:
            return self.xmax - self.xmin
        except TypeError:
            return None

    @property
    def dy(self):
        try:
            return self.ymax - self.ymin
        except TypeError:
            return None

    @property
    def dz(self):
        try:
            return self.zmax - self.zmin
        except TypeError:
            return None

    @property
    def bounds(self):
        return [
            self.xmin, self.xmax, self.ymin, self.ymax, self.zmin, self.zmax
        ]

    @bounds.setter
    def bounds(self, value):
        for b in ['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']:
            setattr(self, b, value.pop(0))

    @property
    def xmin(self):
        return self._xmin

    @xmin.setter
    def xmin(self, value):
        if value is None: self._xmin = None
        else:
            try:
                self._xmin = float(value)
            except:
                raise ValueError('xmin must be a float')

    @property
    def xmax(self):
        return self._xmax

    @xmax.setter
    def xmax(self, value):
        if value is None: self._xmax = None
        else:
            try:
                self._xmax = float(value)
            except:
                raise ValueError('xmax must be a float')

    @property
    def ymin(self):
        return self._ymin

    @ymin.setter
    def ymin(self, value):
        if value is None: self._ymin = None
        else:
            try:
                self._ymin = float(value)
            except:
                raise ValueError('ymin must be a float')

    @property
    def ymax(self):
        return self._ymax

    @ymax.setter
    def ymax(self, value):
        if value is None: self._ymax = None
        else:
            try:
                self._ymax = float(value)
            except:
                raise ValueError('ymax must be a float')

    @property
    def zmin(self):
        return self._zmin

    @zmin.setter
    def zmin(self, value):
        if value is None: self._zmin = None
        else:
            try:
                self._zmin = float(value)
            except:
                raise ValueError('zmin must be a float')

    @property
    def zmax(self):
        return self._zmax

    @zmax.setter
    def zmax(self, value):
        if value is None: self._zmax = None
        else:
            try:
                self._zmax = float(value)
            except:
                raise ValueError('zmax must be a float')

    @property
    def bounded(self):
        return all(d is not None for d in [self.dx, self.dy, self.dz])

    ############################################################################

    @property
    def color(self):
        return self._color

    @color.setter
    def color(self, rgb):
        named = {
            'red': (255, 0, 0),
            'blue': (0, 0, 255),
            'green': (0, 255, 0),
            'white': (255, 255, 255),
            'grey': (128, 128, 128),
            'black': (0, 0, 0),
            'yellow': (255, 255, 0),
            'cyan': (0, 255, 255),
            'magenta': (255, 0, 255),
            'teal': (0, 255, 255),
            'pink': (255, 0, 255),
            'brown': (145, 82, 45),
            'tan': (125, 90, 60),
            'navy': (0, 0, 128)
        }
        if type(rgb) is str and rgb in named:
            self._color = named[rgb]
        elif type(rgb) in [tuple, list] and len(rgb) == 3:
            self._color = tuple(rgb)
        elif rgb is None:
            self._color = rgb
        else:
            raise ValueError(
                'Invalid color (must be integer 3-value tuple or keyword)')

    ############################################################################

    @staticmethod
    def wrap(value):
        ''' Converts a value to a MathTree.

            None values are left alone,
            Strings are assumed to be valid math strings and wrapped
            Floats / ints are converted'''
        if isinstance(value, MathTree):
            return value
        elif value is None:
            return value
        elif type(value) is str:
            return MathTree(value)
        elif type(value) is not float:
            try:
                value = float(value)
            except (ValueError, TypeError):
                raise TypeError('Wrong type for MathTree arithmetic (%s)' %
                                type(value))
        return MathTree.Constant(value)

    @classmethod
    @forcetree
    def min(cls, A, B):
        return cls('i' + A.math + B.math)

    @classmethod
    @forcetree
    def max(cls, A, B):
        return cls('a' + A.math + B.math)

    @classmethod
    @forcetree
    def pow(cls, A, B):
        return cls('p' + A.math + B.math)

    @classmethod
    @forcetree
    def sqrt(cls, A):
        return cls('r' + A.math)

    @classmethod
    @forcetree
    def abs(cls, A):
        return cls('b' + A.math)

    @classmethod
    @forcetree
    def square(cls, A):
        return cls('q' + A.math)

    @classmethod
    @forcetree
    def sin(cls, A):
        return cls('s' + A.math)

    @classmethod
    @forcetree
    def cos(cls, A):
        return cls('c' + A.math)

    @classmethod
    @forcetree
    def tan(cls, A):
        return cls('t' + A.math)

    @classmethod
    @forcetree
    def asin(cls, A):
        return cls('S' + A.math)

    @classmethod
    @forcetree
    def acos(cls, A):
        return cls('C' + A.math)

    @classmethod
    @forcetree
    def atan(cls, A):
        return cls('T' + A.math)

    #########################
    #  MathTree Arithmetic  #
    #########################

    # If shape is set, then + and - perform logical combination;
    # otherwise, they perform arithmeic.
    @matching
    @forcetree
    def __add__(self, rhs):
        if self.shape or (rhs and rhs.shape):

            if rhs is None: return self.clone()

            t = MathTree('i' + self.math + rhs.math, True)

            if self.dx is not None and rhs.dx is not None:
                t.xmin = min(self.xmin, rhs.xmin)
                t.xmax = max(self.xmax, rhs.xmax)
            if self.dx is not None and rhs.dy is not None:
                t.ymin = min(self.ymin, rhs.ymin)
                t.ymax = max(self.ymax, rhs.ymax)
            if self.dz is not None and rhs.dz is not None:
                t.zmin = min(self.zmin, rhs.zmin)
                t.zmax = max(self.zmax, rhs.zmax)

            return t
        else:
            return MathTree('+' + self.math + rhs.math)

    @matching
    @forcetree
    def __radd__(self, lhs):
        if lhs is None: return self.clone()

        if self.shape or (lhs and lhs.shape):

            t = MathTree('i' + lhs.math + self.math)
            if self.dx is not None and lhs.dx is not None:
                t.xmin = min(self.xmin, lhs.xmin)
                t.xmax = max(self.xmax, lhs.xmax)
            if self.dy is not None and lhs.dy is not None:
                t.ymin = min(self.ymin, lhs.ymin)
                t.ymax = max(self.ymax, lhs.ymax)
            if self.dz is not None and lhs.dz is not None:
                t.zmin = min(self.zmin, lhs.zmin)
                t.zmax = max(self.zmax, lhs.zmax)
            return t
        else:
            return MathTree('+' + lhs.math + self.math)

    @matching
    @forcetree
    def __sub__(self, rhs):
        if self.shape or (rhs and rhs.shape):

            if rhs is None: return self.clone()

            t = MathTree('a' + self.math + 'n' + rhs.math, True)
            for i in ['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']:
                setattr(t, i, getattr(self, i))
            return t
        else:
            return MathTree('-' + self.math + rhs.math)

    @matching
    @forcetree
    def __rsub__(self, lhs):
        if self.shape or (lhs and lhs.shape):

            if lhs is None: return MathTree('n' + self.math)

            t = MathTree('a' + lhs.math + 'n' + self.math, True)
            for i in ['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax']:
                setattr(t, i, getattr(lhs, i))
            return t
        else:
            return MathTree('-' + lhs.math + self.math)

    @matching
    @forcetree
    def __and__(self, rhs):
        if self.shape or rhs.shape:
            t = MathTree('a' + self.math + rhs.math, True)
            if self.dx is not None and rhs.dx is not None:
                t.xmin = max(self.xmin, rhs.xmin)
                t.xmax = min(self.xmax, rhs.xmax)
            if self.dy is not None and rhs.dy is not None:
                t.ymin = max(self.ymin, rhs.ymin)
                t.ymax = min(self.ymax, rhs.ymax)
            if self.dz is not None and rhs.dz is not None:
                t.zmin = max(self.zmin, rhs.zmin)
                t.zmax = min(self.zmax, rhs.zmax)
            return t
        else:
            raise NotImplementedError(
                '& operator is undefined for non-shape math expressions.')

    @matching
    @forcetree
    def __rand__(self, lhs):
        if self.shape or lhs.shape:
            t = MathTree('a' + lhs.math + self.math, True)
            if self.dx is not None and lhs.dx is not None:
                t.xmin = max(self.xmin, lhs.xmin)
                t.xmax = min(self.xmax, lhs.xmax)
            if self.dy is not None and lhs.dy is not None:
                t.ymin = max(self.ymin, lhs.ymin)
                t.ymax = min(self.ymax, lhs.ymax)
            if self.dz is not None and lhs.dz is not None:
                t.zmin = max(self.zmin, lhs.zmin)
                t.zmax = min(self.zmax, lhs.zmax)
            return t
        else:
            raise NotImplementedError(
                '& operator is undefined for non-shape math expressions.')

    @matching
    @forcetree
    def __or__(self, rhs):
        if self.shape or rhs.shape:
            t = MathTree('i' + self.math + rhs.math, True)
            if self.dx is not None and rhs.dx is not None:
                t.xmin = min(self.xmin, rhs.xmin)
                t.xmax = max(self.xmax, rhs.xmax)
            if self.dy is not None and rhs.dy is not None:
                t.ymin = min(self.ymin, rhs.ymin)
                t.ymax = max(self.ymax, rhs.ymax)
            if self.dz is not None and rhs.dz is not None:
                t.zmin = min(self.zmin, rhs.zmin)
                t.zmax = max(self.zmax, rhs.zmax)
            return t
        else:
            raise NotImplementedError(
                '| operator is undefined for non-shape math expressions.')

    @matching
    @forcetree
    def __ror__(self, lhs):
        if self.shape or lhs.shape:
            t = MathTree('i' + lhs.math + self.math, True)
            if self.dx is not None and lhs.dx is not None:
                t.xmin = min(self.xmin, lhs.xmin)
                t.xmax = max(self.xmax, lhs.xmax)
            if self.dy is not None and lhs.dy is not None:
                t.ymin = min(self.ymin, lhs.ymin)
                t.ymax = max(self.ymax, lhs.ymax)
            if self.dz is not None and lhs.dz is not None:
                t.zmin = min(self.zmin, lhs.zmin)
                t.zmax = max(self.zmax, lhs.zmax)
            return t
        else:
            raise NotImplementedError(
                '| operator is undefined for non-shape math expressions.')

    @forcetree
    def __mul__(self, rhs):
        return MathTree('*' + self.math + rhs.math)

    @forcetree
    def __rmul__(self, lhs):
        return MathTree('*' + lhs.math + self.math)

    @forcetree
    def __div__(self, rhs):
        return MathTree('/' + self.math + rhs.math)

    @forcetree
    def __rdiv__(self, lhs):
        return MathTree('/' + lhs.math + self.math)

    @forcetree
    def __neg__(self):
        return MathTree('n' + self.math, shape=self.shape)

    ###############################
    ## String and representation ##
    ###############################

    def __str__(self):
        if self._str is None:
            self._str = self.make_str()
        return self._str

    def make_str(self, verbose=False):
        """ @brief Converts the object into an infix-notation string

            @details
            Creates a OS pipe, instructs the object to print itself into the pipe, and reads the output in chunks of maximum size 1024.
        """

        # Create a pipe to get the printout
        read, write = os.pipe()

        # Start the print function running in a separate thread
        # (so that we can eat the output and avoid filling the pipe)
        if verbose: printer = libfab.fdprint_tree_verbose
        else: printer = libfab.fdprint_tree
        t = threading.Thread(target=printer, args=(self.ptr, write))
        t.daemon = True
        t.start()

        s = r = os.read(read, 1024)
        while r:
            r = os.read(read, 1024)
            s += r
        t.join()

        os.close(read)

        return s

    def __repr__(self):
        return "'%s' (tree at %s)" % (self, hex(self.ptr.value))

    def verbose(self):
        return self.make_str(verbose=True)

    def save_dot(self, filename, arrays=False):
        """ @brief Converts math expression to .dot graph description
        """
        if arrays:
            libfab.dot_arrays(self.ptr, filename)
        else:
            libfab.dot_tree(self.ptr, filename)

    @property
    def node_count(self):
        return libfab.count_nodes(self.ptr)

    #################################
    ## Tree manipulation functions ##
    #################################
    @forcetree
    def map(self, X=None, Y=None, Z=None):
        """ @brief Applies a map operator to a tree
            @param X New X function or None
            @param Y New Y function or None
            @param Z New Z function or None
        """
        return MathTree('m' + (X.math if X else ' ') + (Y.math if Y else ' ') +
                        (Z.math if Z else ' ') + self.math,
                        shape=self.shape,
                        color=self.color)

    @forcetree
    def map_bounds(self, X=None, Y=None, Z=None):
        """ @brief Calculates remapped bounds
            @returns Array of remapped bounds
            @param X New X function or None
            @param Y New Y function or None
            @param Z New Z function or None
            @details Note that X, Y, and Z should be the inverse
            of a coordinate mapping to properly transform bounds.
        """
        if self.dx is not None: x = Interval(self.xmin, self.xmax)
        else: x = Interval(float('nan'))
        if self.dy is not None: y = Interval(self.ymin, self.ymax)
        else: y = Interval(float('nan'))
        if self.dz is not None: z = Interval(self.zmin, self.zmax)
        else: z = Interval(float('nan'))

        if self.dx is not None: a = Interval(self.xmin, self.xmax)
        else: a = Interval(float('nan'))
        if self.dy is not None: b = Interval(self.ymin, self.ymax)
        else: b = Interval(float('nan'))
        if self.dz is not None: c = Interval(self.zmin, self.zmax)
        else: c = Interval(float('nan'))

        if X:
            X_p = libfab.make_packed(X.ptr)
            a = libfab.eval_i(X_p, x, y, z)
            libfab.free_packed(X_p)

        if Y:
            Y_p = libfab.make_packed(Y.ptr)
            b = libfab.eval_i(Y_p, x, y, z)
            libfab.free_packed(Y_p)

        if Z:
            Z_p = libfab.make_packed(Z.ptr)
            c = libfab.eval_i(Z_p, x, y, z)
            libfab.free_packed(Z_p)

        bounds = []
        for i in [a, b, c]:
            if math.isnan(i.lower) or math.isnan(i.upper):
                bounds += [None, None]
            else:
                bounds += [i.lower, i.upper]

        return bounds

    @threadsafe
    def clone(self):
        m = MathTree(self.math, shape=self.shape, color=self.color)
        m.bounds = [b for b in self.bounds]
        if self._ptr is not None:
            m._ptr = libfab.clone_tree(self._ptr)
        return m

    #################################
    #    Rendering functions        #
    #################################

    def render(self,
               region=None,
               resolution=None,
               mm_per_unit=None,
               threads=8,
               interrupt=None):
        """ @brief Renders a math tree into an Image
            @param region Evaluation region (if None, taken from expression bounds)
            @param resolution Render resolution in voxels/unit
            @param mm_per_unit Real-world scale
            @param threads Number of threads to use
            @param interrupt threading.Event that aborts rendering if set
            @returns Image data structure
        """

        if region is None:
            if self.dx is None or self.dy is None:
                raise Exception('Unknown render region!')
            elif resolution is None:
                raise Exception('Region or resolution must be provided!')
            region = Region(
                (self.xmin, self.ymin, self.zmin if self.zmin else 0),
                (self.xmax, self.ymax, self.zmax if self.zmax else 0),
                resolution)

        try:
            float(mm_per_unit)
        except ValueError, TypeError:
            raise ValueError('mm_per_unit must be a number')

        if interrupt is None: interrupt = threading.Event()
        halt = ctypes.c_int(0)  # flag to abort render
        image = Image(
            region.ni,
            region.nj,
            channels=1,
            depth=16,
        )

        # Divide the task to share among multiple threads
        clones = [self.clone() for i in range(threads)]
        packed = [libfab.make_packed(c.ptr) for c in clones]

        subregions = region.split_xy(threads)

        # Solve each region in a separate thread
        args = zip(packed, subregions, [image.pixels] * threads,
                   [halt] * threads)

        multithread(libfab.render16, args, interrupt, halt)

        for p in packed:
            libfab.free_packed(p)

        image.xmin = region.X[0] * mm_per_unit
        image.xmax = region.X[region.ni] * mm_per_unit
        image.ymin = region.Y[0] * mm_per_unit
        image.ymax = region.Y[region.nj] * mm_per_unit
        image.zmin = region.Z[0] * mm_per_unit
        image.zmax = region.Z[region.nk] * mm_per_unit

        return image