Skip to content
Snippets Groups Projects
import_img.py 23.3 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_properties:
    
        """ keeps track of image attributes throughout the hirise_dtm_importer 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_importer(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
    
        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
    
        ############################################################################
        ## 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
    
          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
        ############################################################################
    
        def bin2(self, image_iter, bin2_method_type="SLOW"):
    
          """ this is an iterator that: Given an image iterator will yield binned lines """
    
          ignore_value = self.__ignore_value
    
    
          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 != ignore_value and b != ignore_value and (a + b)/2 or 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 = []
    
            else:
              last_line = line
              line_count += 1
    
    
        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...
    
          IGNORE_VALUE = self.__ignore_value
    
    
          base = 0
          for i in range(0, len(raw_data[0])//6):
    
    
            ints = (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] )
    
            ints = [num for num in ints if num != IGNORE_VALUE]
    
    
            # If we have all pesky values, return a pesky value
    
            if not ints:
              binned_data.append( IGNORE_VALUE )
    
              binned_data.append( sum(ints, 0.0) / len(ints) )
    
          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
    
        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]
    
        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]:
            XSize = processed_dims[0]
            XOffset = 0
          if YSize + YOffset > processed_dims[1]:
            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
    
        def getImage(self, img, img_props):
    
          """ Assumes 32-bit pixels -- bins image """
    
          dims = img_props.dims()
    
          # 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 = []
            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 )
    
        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)
    
    
          # 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))
    
        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.append( (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.
    
          # 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
    
            # Just add all points blindly
            # TODO: turn this into a map
            x = 0
            for z in dtm_line:
              if z != None:
    
                coords.append( (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.append( (
    
                  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
    
          me = bpy.data.meshes.new(img_props.name()) # create a new mesh
    
          #from_pydata(self, vertices, edges, faces)
          #Make a mesh from a list of vertices/edges/faces
          #Until we have a nicer way to make geometry, use this.
          #:arg vertices:
          #   float triplets each representing (X, Y, Z)
          #   eg: [(0.0, 1.0, 0.5), ...].
          #:type vertices: iterable object
          #:arg edges:
          #   int pairs, each pair contains two indices to the
          #   *vertices* argument. eg: [(1, 2), ...]
          #:type edges: iterable object
          #:arg faces:
          #   iterator of faces, each faces contains three or more indices to
          #   the *vertices* argument. eg: [(5, 6, 8, 9), (1, 2, 3), ...]
          #:type faces: iterable object
          me.from_pydata(coords, [], faces)      
    
          # me.vertices.add(len(coords)/3)
          # me.vertices.foreach_set("co", coords)
          # me.faces.add(len(faces)/4)
          # me.faces.foreach_set("vertices_raw", faces)
    
    
          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
    
        ################################################################################
    
        #  Yay, done with importer functions ... let's see the abstraction in action!    #
    
        ################################################################################
        def execute(self):
    
          img = open(self.__filepath, 'rb')
    
          (label, parsedLabel) = self.getPDSLabel(img)
    
          image_dims = self.getLinesAndSamples(parsedLabel)
          img_min_max_vals = self.getValidMinMax(parsedLabel)
          self.__ignore_value = self.getMissingConstant(parsedLabel)
    
    
          # 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_properties( 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
          ob_new = self.genMesh(image_iter)
    
          if img:
            img.close()
    
          # Add mesh object to the current scene
          scene = self.__context.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
    
          return ('FINISHED',)
    
    
    def load(operator, context, filepath, scale, bin_mode, cropVars):
    
        print("Bin Mode: %s" % bin_mode)
        print("Scale: %f" % scale)
    
        importer = hirise_dtm_importer(context,filepath)
        importer.bin_mode( bin_mode )
        importer.scale( scale )
    
            importer.crop( cropVars[0], cropVars[1], cropVars[2], cropVars[3] )
        importer.execute()
    
    
        print("Loading %s" % filepath)
        return {'FINISHED'}