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
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
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
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 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
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
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
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