# ##### 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 * 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]) 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 ) else: binned_data.append( sum(ints, 0.0) / 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 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() if XSize is None: XSize = processed_dims[0] if YSize is None: 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) me.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 ################################################################################ # 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 ) if cropVars: importer.crop( cropVars[0], cropVars[1], cropVars[2], cropVars[3] ) importer.execute() print("Loading %s" % filepath) return {'FINISHED'}