Skip to content
Snippets Groups Projects
import_img.py 25.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### BEGIN GPL LICENSE BLOCK #####
    #
    #  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 2
    #  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, write to the Free Software Foundation,
    #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    #
    # ##### END GPL LICENSE BLOCK #####
    
    """
    This script can import a HiRISE DTM .IMG file.
    """
    
    import bpy
    from bpy.props import *
    
    
    Campbell Barton's avatar
    Campbell Barton committed
    from struct import pack, unpack
    
    import os
    import queue, threading
    
    class image_props:
        ''' keeps track of image attributes throughout the hirise_dtm_helper class '''
        def __init__(self, name, dimensions, pixel_scale):
          self.name( name )
          self.dims( dimensions )
          self.processed_dims( dimensions )
          self.pixel_scale( pixel_scale )
    
        def dims(self, dims=None):
          if dims is not None:
            self.__dims = dims
          return self.__dims
    
        def processed_dims(self, processed_dims=None):
          if processed_dims is not None:
            self.__processed_dims = processed_dims
          return self.__processed_dims
    
        def name(self, name=None):
          if name is not None:
            self.__name = name
          return self.__name
    
        def pixel_scale(self, pixel_scale=None):
          if pixel_scale is not None:
            self.__pixel_scale = pixel_scale
          return self.__pixel_scale
    
    class hirise_dtm_helper(object):
        ''' methods to understand/import a HiRISE DTM formatted as a PDS .IMG '''
    
        def __init__(self, context, filepath):
          self.__context = context
          self.__filepath = filepath
          self.__ignore_value = 0x00000000
          self.__bin_mode = 'BIN6'
          self.scale( 1.0 )
          self.__cropXY = False
          self.marsRed(False)
    
        def bin_mode(self, bin_mode=None):
          if bin_mode != None:
            self.__bin_mode = bin_mode
          return self.__bin_mode
    
        def scale(self, scale=None):
          if scale is not None:
            self.__scale = scale
          return self.__scale
    
        def crop(self, widthX, widthY, offX, offY):
          self.__cropXY = [ widthX, widthY, offX, offY ]
          return self.__cropXY
    
        def marsRed(self, marsRed=None):
          if marsRed is not None:
            self.__marsRed = marsRed
          return self.__marsRed
    
        def dbg(self, mesg):
          print(mesg)
    
        ############################################################################
        ## PDS Label Operations
        ############################################################################
    
        def parsePDSLabel(self, labelIter, currentObjectName=None, level = ""):
          # Let's parse this thing... semi-recursively
          ## I started writing this caring about everything in the PDS standard but ...
          ## it's a mess and I only need a few things -- thar be hacks below
          ## Mostly I just don't care about continued data from previous lines
          label_structure = []
    
          # When are we done with this level?
          endStr = "END"
    
          if not currentObjectName is None:
    
            endStr = "END_OBJECT = %s" % currentObjectName
          line = ""
    
          while not line.rstrip() == endStr:
            line = next(labelIter)
    
            # Get rid of comments
            comment = line.find("/*")
            if comment > -1:
              line = line[:comment]
    
            # Take notice of objects
            if line[:8] == "OBJECT =":
              objName = line[8:].rstrip()
              label_structure.append(
                (
                 objName.lstrip().rstrip(),
                 self.parsePDSLabel(labelIter, objName.lstrip().rstrip(), level + "  ")
                )
              )
            elif line.find("END_OBJECT =") > -1:
              pass
            elif len(line.rstrip().lstrip()) > 0:
              key_val = line.split(" = ", 2)
              if len(key_val) == 2:
                label_structure.append( (key_val[0].rstrip().lstrip(), key_val[1].rstrip().lstrip()) )
    
          return label_structure
    
        # There has got to be a better way in python?
        def iterArr(self, label):
          for line in label:
            yield line
    
        def getPDSLabel(self, img):
          # Just takes file and stores it into an array for later use
          label = []
          done = False;
          # Grab label into array of lines
          while not done:
            line = str(img.readline(), 'utf-8')
            if line.rstrip() == "END":
              done = True
            label.append(line)
          return (label, self.parsePDSLabel(self.iterArr(label)))
    
        def getLinesAndSamples(self, label):
          ''' uses the parsed PDS Label to get the LINES and LINE_SAMPLES parameters
              from the first object named "IMAGE" -- is hackish
          '''
          lines = None
          line_samples = None
          for obj in label:
            if obj[0] == "IMAGE":
              return self.getLinesAndSamples(obj[1])
            if obj[0] == "LINES":
              lines = int(obj[1])
            if obj[0] == "LINE_SAMPLES":
              line_samples = int(obj[1])
    
          return ( line_samples, lines )
    
        def getValidMinMax(self, label):
          ''' uses the parsed PDS Label to get the VALID_MINIMUM and VALID_MAXIMUM parameters
              from the first object named "IMAGE" -- is hackish
          '''
          for obj in label:
            if obj[0] == "IMAGE":
              return self.getValidMinMax(obj[1])
            if obj[0] == "VALID_MINIMUM":
              vmin = float(obj[1])
            if obj[0] == "VALID_MAXIMUM":
              vmax = float(obj[1])
    
    
    Campbell Barton's avatar
    Campbell Barton committed
          return vmin, vmax
    
    
        def getMissingConstant(self, label):
          ''' uses the parsed PDS Label to get the MISSING_CONSTANT parameter
              from the first object named "IMAGE" -- is hackish
          '''
          for obj in label:
            if obj[0] == "IMAGE":
              return self.getMissingConstant(obj[1])
            if obj[0] == "MISSING_CONSTANT":
              bit_string_repr = obj[1]
    
          # This is always the same for a HiRISE image, so we are just checking it
          # to be a little less insane here. If someone wants to support another
          # constant then go for it. Just make sure this one continues to work too
          pieces = bit_string_repr.split("#")
          if pieces[0] == "16" and pieces[1] == "FF7FFFFB":
            ignore_value = unpack("f", pack("I", 0xFF7FFFFB))[0]
    
          return ( ignore_value )
    
        ############################################################################
        ## Image operations
        ############################################################################
    
        # decorator to run a generator in a thread
        def threaded_generator(func):
          def start(*args,**kwargs):
            # Setup a queue of returned items
            yield_q = queue.Queue()
            # Thread to run generator inside of
            def worker():
              for obj in func(*args,**kwargs): yield_q.put(obj)
              yield_q.put(StopIteration)
            t = threading.Thread(target=worker)
            t.start()
            # yield from the queue as fast as we can
            obj = yield_q.get()
            while obj is not StopIteration:
              yield obj
              obj = yield_q.get()
    
          # return the thread-wrapped generator
          return start
    
        @threaded_generator
        def bin2(self, image_iter, bin2_method_type="SLOW"):
          ''' this is an iterator that: Given an image iterator will yield binned lines '''
    
          img_props = next(image_iter)
          # dimensions shrink as we remove pixels
          processed_dims = img_props.processed_dims()
          processed_dims = ( processed_dims[0]//2, processed_dims[1]//2 )
          img_props.processed_dims( processed_dims )
          # each pixel is larger as binning gets larger
          pixel_scale = img_props.pixel_scale()
          pixel_scale = ( pixel_scale[0]*2, pixel_scale[1]*2 )
          img_props.pixel_scale( pixel_scale )
          yield img_props
    
          # Take two lists  [a1, a2, a3], [b1, b2, b3] and combine them into one
          # list of [a1 + b1, a2+b2,  ... ] as long as both values are not ignorable
          combine_fun = lambda a, b: a != self.__ignore_value and b != self.__ignore_value and a + b or self.__ignore_value
    
          line_count = 0
          ret_list = []
          for line in image_iter:
            if line_count == 1:
              line_count = 0
              tmp_list = list(map(combine_fun, line, last_line))
              while len(tmp_list) > 1:
                ret_list.append( combine_fun( tmp_list[0], tmp_list[1] ) )
                del tmp_list[0:2]
              yield ret_list
              ret_list = []
    
    Campbell Barton's avatar
    Campbell Barton committed
            # last_line = line  # UNUSED
    
            line_count += 1
    
        @threaded_generator
        def bin6(self, image_iter, bin6_method_type="SLOW"):
          ''' this is an iterator that: Given an image iterator will yield binned lines '''
    
          img_props = next(image_iter)
          # dimensions shrink as we remove pixels
          processed_dims = img_props.processed_dims()
          processed_dims = ( processed_dims[0]//6, processed_dims[1]//6 )
          img_props.processed_dims( processed_dims )
          # each pixel is larger as binning gets larger
          pixel_scale = img_props.pixel_scale()
          pixel_scale = ( pixel_scale[0]*6, pixel_scale[1]*6 )
          img_props.pixel_scale( pixel_scale )
          yield img_props
    
          if bin6_method_type == "FAST":
            bin6_method = self.bin6_real_fast
          else:
            bin6_method = self.bin6_real
    
          raw_data = []
          line_count = 0
          for line in image_iter:
            raw_data.append( line )
            line_count += 1
            if line_count == 6:
              yield bin6_method( raw_data )
              line_count = 0
              raw_data = []
    
        def bin6_real(self, raw_data):
          ''' does a 6x6 sample of raw_data and returns a single line of data '''
          # TODO: make this more efficient
    
          binned_data = []
    
          # Filter out those unwanted hugely negative values...
          filter_fun = lambda a: self.__ignore_value.__ne__(a)
    
          base = 0
          for i in range(0, len(raw_data[0])//6):
    
            ints = list(filter( filter_fun, raw_data[0][base:base+6] +
              raw_data[1][base:base+6] +
              raw_data[2][base:base+6] +
              raw_data[3][base:base+6] +
              raw_data[4][base:base+6] +
              raw_data[5][base:base+6] ))
            len_ints = len( ints )
    
            # If we have all pesky values, return a pesky value
            if len_ints == 0:
              binned_data.append( self.__ignore_value )
            else:
              binned_data.append( sum(ints) / len(ints) )
    
            base += 6
          return binned_data
    
        def bin6_real_fast(self, raw_data):
          ''' takes a single value from each 6x6 sample of raw_data and returns a single line of data '''
          # TODO: make this more efficient
    
          binned_data = []
    
          base = 0
          for i in range(0, len(raw_data[0])//6):
            binned_data.append( raw_data[0][base] )
            base += 6
    
          return binned_data
    
        @threaded_generator
        def bin12(self, image_iter, bin12_method_type="SLOW"):
          ''' this is an iterator that: Given an image iterator will yield binned lines '''
    
          img_props = next(image_iter)
          # dimensions shrink as we remove pixels
          processed_dims = img_props.processed_dims()
          processed_dims = ( processed_dims[0]//12, processed_dims[1]//12 )
          img_props.processed_dims( processed_dims )
          # each pixel is larger as binning gets larger
          pixel_scale = img_props.pixel_scale()
          pixel_scale = ( pixel_scale[0]*12, pixel_scale[1]*12 )
          img_props.pixel_scale( pixel_scale )
          yield img_props
    
          if bin12_method_type == "FAST":
            bin12_method = self.bin12_real_fast
          else:
            bin12_method = self.bin12_real
    
          raw_data = []
          line_count = 0
          for line in image_iter:
            raw_data.append( line )
            line_count += 1
            if line_count == 12:
              yield bin12_method( raw_data )
              line_count = 0
              raw_data = []
    
        def bin12_real(self, raw_data):
          ''' does a 12x12 sample of raw_data and returns a single line of data '''
    
          binned_data = []
    
          # Filter out those unwanted hugely negative values...
          filter_fun = lambda a: self.__ignore_value.__ne__(a)
    
          base = 0
          for i in range(0, len(raw_data[0])//12):
    
            ints = list(filter( filter_fun, raw_data[0][base:base+12] +
              raw_data[1][base:base+12] +
              raw_data[2][base:base+12] +
              raw_data[3][base:base+12] +
              raw_data[4][base:base+12] +
              raw_data[5][base:base+12] +
              raw_data[6][base:base+12] +
              raw_data[7][base:base+12] +
              raw_data[8][base:base+12] +
              raw_data[9][base:base+12] +
              raw_data[10][base:base+12] +
              raw_data[11][base:base+12] ))
            len_ints = len( ints )
    
            # If we have all pesky values, return a pesky value
            if len_ints == 0:
              binned_data.append( self.__ignore_value )
            else:
              binned_data.append( sum(ints) / len(ints) )
    
            base += 12
          return binned_data
    
        def bin12_real_fast(self, raw_data):
          ''' takes a single value from each 12x12 sample of raw_data and returns a single line of data '''
          return raw_data[0][11::12]
    
        @threaded_generator
        def cropXY(self, image_iter, XSize=None, YSize=None, XOffset=0, YOffset=0):
          ''' return a cropped portion of the image '''
    
          img_props = next(image_iter)
          # dimensions shrink as we remove pixels
          processed_dims = img_props.processed_dims()
    
    
            YSize = processed_dims[1]
    
          if XSize + XOffset > processed_dims[0]:
            self.dbg("WARNING: Upstream dims are larger than cropped XSize dim")
            XSize = processed_dims[0]
            XOffset = 0
          if YSize + YOffset > processed_dims[1]:
            self.dbg("WARNING: Upstream dims are larger than cropped YSize dim")
            YSize = processed_dims[1]
            YOffset = 0
    
          img_props.processed_dims( (XSize, YSize) )
          yield img_props
    
          currentY = 0
          for line in image_iter:
            if currentY >= YOffset and currentY <= YOffset + YSize:
              yield line[XOffset:XOffset+XSize]
            # Not much point in reading the rest of the data...
            if currentY == YOffset + YSize:
              return
            currentY += 1
    
        @threaded_generator
        def getImage(self, img, img_props):
          ''' Assumes 32-bit pixels -- bins image '''
          dims = img_props.dims()
          self.dbg("getting image (x,y): %d,%d" % ( dims[0], dims[1] ))
    
          # setup to unpack more efficiently.
          x_len = dims[0]
          # little endian (PC_REAL)
          unpack_str = "<"
          # unpack_str = ">"
          unpack_bytes_str = "<"
          pack_bytes_str = "="
          # 32 bits/sample * samples/line = y_bytes (per line)
          x_bytes = 4*x_len
          for x in range(0, x_len):
            # 32-bit float is "d"
            unpack_str += "f"
            unpack_bytes_str += "I"
            pack_bytes_str += "I"
    
          # Each iterator yields this first ... it is for reference of the next iterator:
          yield img_props
    
          for y in range(0, dims[1]):
            # pixels is a byte array
            pixels = b''
            while len(pixels) < x_bytes:
              new_pixels = img.read( x_bytes - len(pixels) )
              pixels += new_pixels
              if len(new_pixels) == 0:
                x_bytes = -1
                pixels = []
                self.dbg("Uh oh: unexpected EOF!")
            if len(pixels) == x_bytes:
              if 0 == 1:
                repacked_pixels = b''
                for integer in unpack(unpack_bytes_str, pixels):
                  repacked_pixels += pack("=I", integer)
                yield unpack( unpack_str, repacked_pixels )
              else:
                yield unpack( unpack_str, pixels )
    
        @threaded_generator
        def shiftToOrigin(self, image_iter, image_min_max):
          ''' takes a generator and shifts the points by the valid minimum
              also removes points with value self.__ignore_value and replaces them with None
          '''
    
          # use the passed in values ...
          valid_min = image_min_max[0]
    
          # pass on dimensions/pixel_scale since we don't modify them here
          yield next(image_iter)
    
          self.dbg("shiftToOrigin filter enabled...");
    
          # closures rock!
          def normalize_fun(point):
            if point == self.__ignore_value:
              return None
            return point - valid_min
    
          for line in image_iter:
            yield list(map(normalize_fun, line))
          self.dbg("shifted all points")
    
        @threaded_generator
        def scaleZ(self, image_iter, scale_factor):
          ''' scales the mesh values by a factor '''
          # pass on dimensions since we don't modify them here
          yield next(image_iter)
    
          scale_factor = self.scale()
    
          def scale_fun(point):
            try:
              return point * scale_factor
            except:
              return None
    
          for line in image_iter:
            yield list(map(scale_fun, line))
    
        def genMesh(self, image_iter):
          '''Returns a mesh object from an image iterator this has the
             value-added feature that a value of "None" is ignored
          '''
    
          # Get the output image size given the above transforms
          img_props = next(image_iter)
    
          # Let's interpolate the binned DTM with blender -- yay meshes!
          coords = []
          faces  = []
          face_count = 0
          coord = -1
          max_x = img_props.processed_dims()[0]
          max_y = img_props.processed_dims()[1]
    
          scale_x = self.scale() * img_props.pixel_scale()[0]
          scale_y = self.scale() * img_props.pixel_scale()[1]
    
          line_count = 0
          # seed the last line (or previous line) with a line
          last_line = next(image_iter)
          point_offset = 0
          previous_point_offset = 0
    
          # Let's add any initial points that are appropriate
          x = 0
          point_offset += len( last_line ) - last_line.count(None)
          for z in last_line:
            if z != None:
              coords.extend([x*scale_x, 0.0, z])
              coord += 1
            x += 1
    
          # We want to ignore points with a value of "None" but we also need to create vertices
          # with an index that we can re-create on the next line. The solution is to remember
          # two offsets: the point offset and the previous point offset.
          #   these offsets represent the point index that blender gets -- not the number of
          #   points we have read from the image
    
          # if "x" represents points that are "None" valued then conceptually this is how we
          # think of point indices:
          #
          # previous line: offset0   x   x  +1  +2  +3
          # current line:  offset1   x  +1  +2  +3   x
    
          # once we can map points we can worry about making triangular or square faces to fill
          # the space between vertices so that blender is more efficient at managing the final
          # structure.
    
          self.dbg('generate mesh coords/faces from processed image data...')
    
          # read each new line and generate coordinates+faces
          for dtm_line in image_iter:
    
            # Keep track of where we are in the image
            line_count += 1
            y_val = line_count*-scale_y
            if line_count % 31 == 0:
              self.dbg("reading image... %d of %d" % ( line_count, max_y ))
    
            # Just add all points blindly
            # TODO: turn this into a map
            x = 0
            for z in dtm_line:
              if z != None:
                coords.extend( [x*scale_x, y_val, z] )
                coord += 1
              x += 1
    
            # Calculate faces
            for x in range(0, max_x - 1):
              vals = [
                last_line[ x + 1 ],
                last_line[ x ],
                dtm_line[  x ],
                dtm_line[  x + 1 ],
                ]
    
              # Two or more values of "None" means we can ignore this block
              none_val = vals.count(None)
    
              # Common case: we can create a square face
              if none_val == 0:
                faces.extend( [
                  previous_point_offset,
                  previous_point_offset+1,
                  point_offset+1,
                  point_offset,
                  ] )
                face_count += 1
              elif none_val == 1:
                # special case: we can implement a triangular face
                ## NB: blender 2.5 makes a triangular face when the last coord is 0
                # TODO: implement a triangular face
                pass
    
              if vals[1] != None:
                previous_point_offset += 1
              if vals[2] != None:
                point_offset += 1
    
            # Squeeze the last point offset increment out of the previous line
            if last_line[-1] != None:
              previous_point_offset += 1
    
            # Squeeze the last point out of the current line
            if dtm_line[-1] != None:
              point_offset += 1
    
            # remember what we just saw (and forget anything before that)
            last_line = dtm_line
    
          self.dbg('generate mesh from coords/faces...')
          me = bpy.data.meshes.new(img_props.name()) # create a new mesh
    
          self.dbg('coord: %d' % coord)
          self.dbg('len(coords): %d' % len(coords))
          self.dbg('len(faces): %d' % len(faces))
    
          self.dbg('setting coords...')
          me.vertices.add(len(coords)/3)
          me.vertices.foreach_set("co", coords)
    
          self.dbg('setting faces...')
          me.faces.add(len(faces)/4)
          me.faces.foreach_set("vertices_raw", faces)
    
          self.dbg('running update...')
    
    
          bin_desc = self.bin_mode()
          if bin_desc == 'NONE':
            bin_desc = 'No Bin'
    
          ob=bpy.data.objects.new("DTM - %s" % bin_desc, me)
    
          return ob
    
        def marsRedMaterial(self):
          ''' produce some approximation of a mars surface '''
          mat = None
          for material in bpy.data.materials:
            if material.getName() == "redMars":
              mat = material
          if mat is None:
            mat = bpy.data.materials.new("redMars")
            mat.diffuse_shader = 'MINNAERT'
            mat.setRGBCol(  (0.426, 0.213, 0.136) )
            mat.setDiffuseDarkness(0.8)
            mat.specular_shader = 'WARDISO'
            mat.setSpecCol( (1.000, 0.242, 0.010) )
            mat.setSpec( 0.010 )
            mat.setRms( 0.100 )
          return mat
    
        ################################################################################
        #  Yay, done with helper functions ... let's see the abstraction in action!    #
        ################################################################################
        def execute(self):
    
          self.dbg('opening/importing file: %s' % self.__filepath)
          img = open(self.__filepath, 'rb')
    
          self.dbg('read PDS Label...')
          (label, parsedLabel) = self.getPDSLabel(img)
    
          self.dbg('parse PDS Label...')
          image_dims = self.getLinesAndSamples(parsedLabel)
          img_min_max_vals = self.getValidMinMax(parsedLabel)
          self.__ignore_value = self.getMissingConstant(parsedLabel)
    
          self.dbg('import/bin image data...')
    
          # MAGIC VALUE? -- need to formalize this to rid ourselves of bad points
          img.seek(28)
          # Crop off 4 lines
          img.seek(4*image_dims[0])
    
          # HiRISE images (and most others?) have 1m x 1m pixels
          pixel_scale=(1, 1)
    
          # The image we are importing
          image_name = os.path.basename( self.__filepath )
    
          # Set the properties of the image in a manageable object
          img_props = image_props( image_name, image_dims, pixel_scale )
    
          # Get an iterator to iterate over lines
          image_iter = self.getImage(img, img_props)
    
          ## Wrap the image_iter generator with other generators to modify the dtm on a
          ## line-by-line basis. This creates a stream of modifications instead of reading
          ## all of the data at once, processing all of the data (potentially several times)
          ## and then handing it off to blender
          ## TODO: find a way to alter projection based on transformations below
    
          if self.__cropXY:
            image_iter = self.cropXY(image_iter,
                                     XSize=self.__cropXY[0], 
                                     YSize=self.__cropXY[1],
                                     XOffset=self.__cropXY[2],
                                     YOffset=self.__cropXY[3]
            			 )
    
          # Select an appropriate binning mode
          ## TODO: generalize the binning fn's
          bin_mode = self.bin_mode()
          bin_mode_funcs = {
            'BIN2': self.bin2(image_iter),
            'BIN6': self.bin6(image_iter),
            'BIN6-FAST': self.bin6(image_iter, 'FAST'),
            'BIN12': self.bin12(image_iter),
            'BIN12-FAST': self.bin12(image_iter, 'FAST')
            }
          if bin_mode in bin_mode_funcs.keys():
            image_iter = bin_mode_funcs[ bin_mode ]
    
          image_iter = self.shiftToOrigin(image_iter, img_min_max_vals)
    
          if self.scale != 1.0:
            image_iter = self.scaleZ(image_iter, img_min_max_vals)
    
          # Create a new mesh object and set data from the image iterator
          self.dbg('generating mesh object...')
          ob_new = self.genMesh(image_iter)
    
          if self.marsRed():
            mars_red = self.marsRedMaterial()
            ob_new.materials += [mars_red]
    
          if img:
            img.close()
    
          # Add mesh object to the current scene
          scene = self.__context.scene
          self.dbg('linking object to scene...')
          scene.objects.link(ob_new)
          scene.update()
    
          # deselect other objects
          bpy.ops.object.select_all(action='DESELECT')
    
          # scene.objects.active = ob_new
          # Select the new mesh
          ob_new.select = True
    
          self.dbg('done with ops ... now wait for blender ...')
    
          return ('FINISHED',)
    
    def load(operator, context, filepath, scale, bin_mode, cropVars, marsRed):
        print("Bin Mode: %s" % bin_mode)
        print("Scale: %f" % scale)
        helper = hirise_dtm_helper(context,filepath)
        helper.bin_mode( bin_mode )
        helper.scale( scale )
        if cropVars:
            helper.crop( cropVars[0], cropVars[1], cropVars[2], cropVars[3] )
        helper.execute()
        if marsRed:
            helper.marsRed(marsRed)
    
        print("Loading %s" % filepath)
        return {'FINISHED'}