def test_rpc_multi_back_projection(): model = rpc_from_gdal_dict(rpc_md) img_pts = model.project(points) print("Running multi-point back projection") bp = model.back_project(img_pts, [p[2] for p in points]) print("diff: ", bp - points) assert numpy.max(numpy.abs(bp - points)) < 1e-16
def main(args): parser = argparse.ArgumentParser( description='Projects source_points onto a source_image' ' using an RPC projection read from source_image or from raytheon_rpc') parser.add_argument("source_image", help="Source image file name") parser.add_argument("source_points", help="Source points file name") parser.add_argument("destination_image", help="Destination image file name") parser.add_argument("--raytheon-rpc", type=str, help="Raytheon RPC file name. If not provided, " "the RPC is read from the source_image") parser.add_argument( "--type", choices=["uint8", "uint16", "float32"], help="Specify the type for the height band, default is float32.") args = parser.parse_args(args) # open the GDAL file sourceImage = gdal.Open(args.source_image, gdal.GA_ReadOnly) if not sourceImage: raise RuntimeError("Error: Failed to open source image {}".format( args.source_image)) model = None if (args.raytheon_rpc): # read the RPC from raytheon file print("Reading RPC from Raytheon file: {}".format(args.raytheon_rpc)) model = raytheon_rpc.read_raytheon_rpc_file(args.raytheon_rpc) else: # read the RPC from RPC Metadata in the image file print("Reading RPC Metadata from {}".format(args.source_image)) rpcMetaData = sourceImage.GetMetadata('RPC') model = rpc.rpc_from_gdal_dict(rpcMetaData) driver = sourceImage.GetDriver() driverMetadata = driver.GetMetadata() destImage = None if driverMetadata.get(gdal.DCAP_CREATE) == "YES": print("Create destination image (height is {}), " "size:({}, {}) ...".format( "float32" if not args.type else args.type, sourceImage.RasterXSize, sourceImage.RasterYSize)) # georeference information projection = sourceImage.GetProjection() transform = sourceImage.GetGeoTransform() gcpProjection = sourceImage.GetGCPProjection() gcps = sourceImage.GetGCPs() options = [] # ensure that space will be reserved for geographic corner coordinates # (in DMS) to be set later if (driver.ShortName == "NITF" and not projection): options.append("ICORDS=G") if (args.type == "uint8"): eType = gdal.GDT_Byte dtype = numpy.uint8 MAX_VALUE = 255 elif (args.type == "uint16"): eType = gdal.GDT_UInt16 dtype = numpy.uint16 MAX_VALUE = 65535 else: eType = gdal.GDT_Float32 dtype = numpy.float32 destImage = driver.Create(args.destination_image, xsize=sourceImage.RasterXSize, ysize=sourceImage.RasterYSize, bands=1, eType=eType, options=options) if (projection): # georeference through affine geotransform destImage.SetProjection(projection) destImage.SetGeoTransform(transform) else: # georeference through GCPs destImage.SetGCPs(gcps, gcpProjection) raster = numpy.zeros( (sourceImage.RasterYSize, sourceImage.RasterXSize), dtype=dtype) else: raise RuntimeError( "Error: driver {} does not supports Create().".format(driver)) # read the pdal file and project the points json = u""" { "pipeline": [ "%s", { "type":"filters.reprojection", "out_srs":"EPSG:4326" } ] }""" print("Loading Point Cloud") json = json % args.source_points pipeline = pdal.Pipeline(json) pipeline.validate() # check if our JSON and options were good # this causes a segfault at the end of the program # pipeline.loglevel = 8 # really noisy pipeline.execute() arrays = pipeline.arrays arrayX = arrays[0]['X'] arrayY = arrays[0]['Y'] arrayZ = arrays[0]['Z'] pipeline = None # Sort the points by height so that higher points project last print("Sorting by Height") heightIdx = numpy.argsort(arrayZ) arrayX = arrayX[heightIdx] arrayY = arrayY[heightIdx] arrayZ = arrayZ[heightIdx] minZ = numpy.amin(arrayZ) maxZ = numpy.amax(arrayZ) # project points to get image indexes and save their height into the image print("Project {} points to destination image ...".format(len(arrayX))) print("Points min/max Z: {}/{} ...".format(minZ, maxZ)) print("Projecting Points") imgPoints = model.project( numpy.array([arrayX, arrayY, arrayZ]).transpose()) intImgPoints = imgPoints.astype(numpy.int).transpose() # find indicies of points that fall inside the image bounds validIdx = numpy.logical_and.reduce( (intImgPoints[1] < raster.shape[0], intImgPoints[1] >= 0, intImgPoints[0] < raster.shape[1], intImgPoints[0] >= 0)) # keep only the points that are in the image numOut = numpy.size(validIdx) - numpy.count_nonzero(validIdx) if (numOut > 0): print("Skipped {} points outside of image".format(numOut)) intImgPoints = intImgPoints[:, validIdx] if (args.type == "uint16" or args.type == "uint8"): quantizedZ = ((arrayZ - minZ) * MAX_VALUE / (maxZ - minZ)).astype( numpy.int) quantizedZ = quantizedZ[validIdx] print("Rendering Image") raster[intImgPoints[1], intImgPoints[0]] = quantizedZ else: arrayZ = arrayZ[validIdx] print("Rendering Image") raster[intImgPoints[1], intImgPoints[0]] = arrayZ # Write the image print("Write destination image ...") destImage.GetRasterBand(1).WriteArray(raster) # close files print("Close files ...") destImage = None sourceImage = None
def orthorectify(args_source_image, args_dsm, args_destination_image, args_occlusion_thresh=1.0, args_denoise_radius=2, args_raytheon_rpc=None, args_dtm=None): """ Orthorectify an image given the DSM Args: source_image: Source image file name dsm: Digital surface model (DSM) image file name destination_image: Orthorectified image file name occlusion-thresh: Threshold on height difference for detecting and masking occluded regions (in meters) denoise-radius: Apply morphological operations with this radius to the DSM reduce speckled noise raytheon-rpc: Raytheon RPC file name. If not provided the RPC is read from the source_image Returns: COMPLETE_DSM_INTERSECTION = 0 PARTIAL_DSM_INTERSECTION = 1 EMPTY_DSM_INTERSECTION = 2 ERROR = 10 """ returnValue = COMPLETE_DSM_INTERSECTION # open the source image sourceImage = gdal.Open(args_source_image, gdal.GA_ReadOnly) if not sourceImage: return ERROR sourceBand = sourceImage.GetRasterBand(1) if (args_raytheon_rpc): # read the RPC from raytheon file print("Reading RPC from Raytheon file: {}".format(args_raytheon_rpc)) model = raytheon_rpc.read_raytheon_rpc_file(args_raytheon_rpc) else: # read the RPC from RPC Metadata in the image file print("Reading RPC Metadata from {}".format(args_source_image)) rpcMetaData = sourceImage.GetMetadata('RPC') model = rpc.rpc_from_gdal_dict(rpcMetaData) if model is None: print("Error reading the RPC") return ERROR # open the DSM dsm = gdal.Open(args_dsm, gdal.GA_ReadOnly) if not dsm: return ERROR band = dsm.GetRasterBand(1) dsmRaster = band.ReadAsArray( xoff=0, yoff=0, win_xsize=dsm.RasterXSize, win_ysize=dsm.RasterYSize) dsm_nodata_value = band.GetNoDataValue() print("DSM raster shape {}".format(dsmRaster.shape)) if args_dtm: dtm = gdal.Open(args_dtm, gdal.GA_ReadOnly) if not dtm: return ERROR band = dtm.GetRasterBand(1) dtmRaster = band.ReadAsArray( xoff=0, yoff=0, win_xsize=dtm.RasterXSize, win_ysize=dtm.RasterYSize) newRaster = numpy.where(dsmRaster != dsm_nodata_value, dsmRaster, dtmRaster) dsmRaster = newRaster # apply morphology to denoise the DSM if (args_denoise_radius > 0): morph_struct = circ_structure(args_denoise_radius) dsmRaster = morphology.grey_opening(dsmRaster, structure=morph_struct) dsmRaster = morphology.grey_closing(dsmRaster, structure=morph_struct) # create the rectified image driver = dsm.GetDriver() driverMetadata = driver.GetMetadata() destImage = None arrayX = None arrayY = None arrayZ = None if driverMetadata.get(gdal.DCAP_CREATE) == "YES": print("Create destination image of " "size:({}, {}) ...".format(dsm.RasterXSize, dsm.RasterYSize)) # georeference information projection = dsm.GetProjection() transform = dsm.GetGeoTransform() gcpProjection = dsm.GetGCPProjection() gcps = dsm.GetGCPs() options = ["COMPRESS=DEFLATE"] # ensure that space will be reserved for geographic corner coordinates # (in DMS) to be set later if (driver.ShortName == "NITF" and not projection): options.append("ICORDS=G") # If I try to use AddBand with GTiff I get: # Dataset does not support the AddBand() method. # So I create all bands using the same type at the begining destImage = driver.Create( args_destination_image, xsize=dsm.RasterXSize, ysize=dsm.RasterYSize, bands=sourceImage.RasterCount, eType=sourceBand.DataType, options=options) if (projection): # georeference through affine geotransform destImage.SetProjection(projection) destImage.SetGeoTransform(transform) pixels = numpy.arange(0, dsm.RasterXSize) pixels = numpy.tile(pixels, dsm.RasterYSize) lines = numpy.arange(0, dsm.RasterYSize) lines = numpy.repeat(lines, dsm.RasterXSize) arrayX = transform[0] + pixels * transform[1] + lines * transform[2] arrayY = transform[3] + pixels * transform[4] + lines * transform[5] arrayZ = dsmRaster[lines, pixels] validIdx = arrayZ != dsm_nodata_value pixels = pixels[validIdx] lines = lines[validIdx] arrayX = arrayX[validIdx] arrayY = arrayY[validIdx] arrayZ = arrayZ[validIdx] else: # georeference through GCPs destImage.SetGCPs(gcps, gcpProjection) # not implemented: compute arrayX, arrayY, arrayZ print("Not implemented yet") return ERROR else: print("Driver {} does not supports Create().".format(driver)) return ERROR # convert coordinates to Long/Lat srs = osr.SpatialReference(wkt=projection) proj_srs = srs.ExportToProj4() inProj = pyproj.Proj(proj_srs) outProj = pyproj.Proj('+proj=longlat +datum=WGS84') arrayX, arrayY = pyproj.transform(inProj, outProj, arrayX, arrayY) # Sort the points by height so that higher points project last if (args_occlusion_thresh > 0): print("Sorting by Height") heightIdx = numpy.argsort(arrayZ) arrayX = arrayX[heightIdx] arrayY = arrayY[heightIdx] arrayZ = arrayZ[heightIdx] lines = lines[heightIdx] pixels = pixels[heightIdx] # project the points minZ = numpy.amin(arrayZ) maxZ = numpy.amax(arrayZ) # project points to get image indexes and save their height into the image print("Project {} points to destination image ...".format(len(arrayX))) print("Points min/max Z: {}/{} ...".format(minZ, maxZ)) print("Projecting Points") imgPoints = model.project(numpy.array([arrayX, arrayY, arrayZ]).transpose()) intImgPoints = imgPoints.astype(numpy.int).transpose() # coumpute the bound of the relevant AOI in the source image print("Source Image size: ", [sourceImage.RasterXSize, sourceImage.RasterYSize]) minPoint = numpy.maximum([0, 0], numpy.min(intImgPoints, 1)) print("AOI min: ", minPoint) maxPoint = numpy.minimum(numpy.max(intImgPoints, 1), [sourceImage.RasterXSize, sourceImage.RasterYSize]) print("AOI max: ", maxPoint) cropSize = maxPoint - minPoint if numpy.any(cropSize < 1): print("DSM does not intersect source image") returnValue = EMPTY_DSM_INTERSECTION # shift the projected image point to the cropped AOI space intImgPoints[0] -= minPoint[0] intImgPoints[1] -= minPoint[1] # find indicies of points that fall inside the image bounds print("Source raster shape {}".format(cropSize)) validIdx = numpy.logical_and.reduce((intImgPoints[1] < cropSize[1], intImgPoints[1] >= 0, intImgPoints[0] < cropSize[0], intImgPoints[0] >= 0)) intImgPoints = intImgPoints[:, validIdx] # keep only the points that are in the image numOut = numpy.size(validIdx) - numpy.count_nonzero(validIdx) if (numOut > 0 and not returnValue == EMPTY_DSM_INTERSECTION): print("Skipped {} points outside of image".format(numOut)) returnValue = PARTIAL_DSM_INTERSECTION # use a height map to test for occlusion if (args_occlusion_thresh > 0): print("Mapping occluded points") valid_arrayZ = arrayZ[validIdx] # render a height map in the source image space height_map = numpy.full(cropSize[::-1], -numpy.inf, dtype=numpy.float32) height_map[intImgPoints[1], intImgPoints[0]] = valid_arrayZ # get a mask of points that locally are (approximately) # the highest point in the map is_max_height = height_map[intImgPoints[1], intImgPoints[0]] \ <= valid_arrayZ + args_occlusion_thresh num_occluded = numpy.size(is_max_height) - numpy.count_nonzero(is_max_height) print("Skipped {} occluded points".format(num_occluded)) # keep only non-occluded image points intImgPoints = intImgPoints[:, is_max_height] # disable occluded points in the valid pixel mask validIdx[numpy.nonzero(validIdx)[0][numpy.logical_not(is_max_height)]] = False for bandIndex in range(1, sourceImage.RasterCount + 1): print("Processing band {} ...".format(bandIndex)) sourceBand = sourceImage.GetRasterBand(bandIndex) nodata_value = sourceBand.GetNoDataValue() # for now use zero as a no-data value if one is not specified # it would probably be better to add a mask (alpha) band instead if nodata_value is None: nodata_value = 0 if numpy.any(cropSize < 1): # read one value for data type sourceRaster = sourceBand.ReadAsArray( xoff=0, yoff=0, win_xsize=1, win_ysize=1) destRaster = numpy.full( (dsm.RasterYSize, dsm.RasterXSize), nodata_value, dtype=sourceRaster.dtype) else: sourceRaster = sourceBand.ReadAsArray( xoff=int(minPoint[0]), yoff=int(minPoint[1]), win_xsize=int(cropSize[0]), win_ysize=int(cropSize[1])) print("Copying colors ...") destRaster = numpy.full( (dsm.RasterYSize, dsm.RasterXSize), nodata_value, dtype=sourceRaster.dtype) destRaster[lines[validIdx], pixels[validIdx]] = sourceRaster[ intImgPoints[1], intImgPoints[0]] print("Write band ...") destBand = destImage.GetRasterBand(bandIndex) destBand.SetNoDataValue(nodata_value) destBand.WriteArray(destRaster) return returnValue
def main(args): parser = argparse.ArgumentParser( description="Crop out images for each of the CORE3D AOIs") parser.add_argument("aoi", help="dataset AOI: D1 (WPAFB), D2 (WPAFB), " "D3 (USCD), D4 (Jacksonville) or " "DSM used to get the cropping bounds and elevation") parser.add_argument( "dest_dir", help="Destination directory for writing crops. Crop files have " "the same name as source images + an optional postifx.") parser.add_argument("src_root", help="Source imagery root directory or list of images", nargs="+") parser.add_argument( "--dest_file_postfix", help="Postfix added to destination files, before the extension") parser.add_argument("--rpc_dir", help="Source directory for RPCs or list of RPC files", nargs="+") args = parser.parse_args(args) useDSM = False if os.path.isfile(args.aoi): useDSM = True if os.path.isfile(args.src_root[0]): src_root = args.src_root print('Cropping a list of {} images'.format(len(args.src_root))) else: src_root = os.path.join(args.src_root[0], '') print('Cropping all images from directory: {}'.format(args.src_root)) dest_dir = os.path.join(args.dest_dir, '') if (not args.dest_file_postfix): dest_file_postfix = "_crop" else: dest_file_postfix = args.dest_file_postfix print('Writing crops in directory: ' + dest_dir) print('Cropping to AOI: ' + args.aoi) rpc_dir = None if args.rpc_dir: if os.path.isfile(args.rpc_dir[0]): rpc_dir = args.rpc_dir print('Using a list of {} RPCs.'.format(len(args.rpc_dir))) else: rpc_dir = os.path.join(args.rpc_dir[0], '') print("Using all RPCs from directory: {}".format(rpc_dir)) else: print('Using RPCs from image metadata.') if useDSM: data_dsm = gdal_utils.gdal_open(args.aoi) # Elevation dsm = data_dsm.GetRasterBand(1).ReadAsArray(0, 0, data_dsm.RasterXSize, data_dsm.RasterYSize, buf_type=gdal.GDT_Float32) no_data_value = data_dsm.GetRasterBand(1).GetNoDataValue() dsm_without_no_data = dsm[dsm != no_data_value] elevations = np.array([[dsm_without_no_data.min()], [dsm_without_no_data.max()]]) # Cropping bounds from DSM [minX, minY, maxX, maxY] = gdal_utils.gdal_bounding_box( data_dsm, pyproj.Proj('+proj=longlat +datum=WGS84')) latlong_corners = np.array([[minX, minY, elevations[0]], [maxX, minY, elevations[0]], [maxX, maxY, elevations[0]], [minX, maxY, elevations[0]], [minX, minY, elevations[1]], [maxX, minY, elevations[1]], [maxX, maxY, elevations[1]], [minX, maxY, elevations[1]]]) print("Cropping bounds extracted from DSM") else: elevation_range = 100 # WPAFB AOI D1 if args.aoi == 'D1': elevation = 240 ul_lon = -84.11236693243779 ul_lat = 39.77747025512961 ur_lon = -84.10530109439955 ur_lat = 39.77749705975315 lr_lon = -84.10511182729961 lr_lat = 39.78290042788092 ll_lon = -84.11236485416471 ll_lat = 39.78287156225952 # WPAFB AOI D2 if args.aoi == 'D2': elevation = 300 ul_lon = -84.08847226672408 ul_lat = 39.77650841377968 ur_lon = -84.07992142333644 ur_lat = 39.77652166058358 lr_lon = -84.07959205694203 lr_lat = 39.78413758747398 ll_lon = -84.0882028871317 ll_lat = 39.78430009793551 # UCSD AOI D3 if args.aoi == 'D3': elevation = 120 ul_lon = -117.24298768132505 ul_lat = 32.882791370856857 ur_lon = -117.24296375496185 ur_lat = 32.874021450913411 lr_lon = -117.2323749640905 lr_lat = 32.874041569804469 ll_lon = -117.23239784772379 ll_lat = 32.882811496466012 # Jacksonville AOI D4 if args.aoi == 'D4': elevation = 2 ul_lon = -81.67078466333165 ul_lat = 30.31698808384777 ur_lon = -81.65616946309449 ur_lat = 30.31729872444624 lr_lon = -81.65620275072482 lr_lat = 30.329923847788603 ll_lon = -81.67062242425624 ll_lat = 30.32997669492018 for src_img_file, dst_img_file in filesFromArgs(src_root, dest_dir, dest_file_postfix): dst_file_no_ext = os.path.splitext(dst_img_file)[0] dst_img_file = dst_file_no_ext + ".tif" print('Converting img: ' + src_img_file) src_image = gdal.Open(src_img_file, gdalconst.GA_ReadOnly) nodata_values = [] nodata = 0 for i in range(src_image.RasterCount): nodata_value = src_image.GetRasterBand(i + 1).GetNoDataValue() if not nodata_value: nodata_value = nodata nodata_values.append(nodata_value) if useDSM: poly = latlong_corners.copy() elevation = np.median(dsm) else: poly = np.array([[ul_lon, ul_lat, elevation + elevation_range], [ur_lon, ur_lat, elevation + elevation_range], [lr_lon, lr_lat, elevation + elevation_range], [ll_lon, ll_lat, elevation + elevation_range], [ul_lon, ul_lat, elevation + elevation_range], [ul_lon, ul_lat, elevation - elevation_range], [ur_lon, ur_lat, elevation - elevation_range], [lr_lon, lr_lat, elevation - elevation_range], [ll_lon, ll_lat, elevation - elevation_range], [ul_lon, ul_lat, elevation - elevation_range]]) if rpc_dir: print("Using file RPC: {}".format(rpc_dir)) model = read_raytheon_RPC(rpc_dir, src_img_file) if model is None: print('No RPC file exists using image metadata RPC: ' + src_img_file + '\n') rpc_md = src_image.GetMetadata('RPC') model = rpc.rpc_from_gdal_dict(rpc_md) else: rpc_md = rpc.rpc_to_gdal_dict(model) else: print("Using image RPC.") rpc_md = src_image.GetMetadata('RPC') model = rpc.rpc_from_gdal_dict(rpc_md) # Project the world point locations into the image pixel_poly = model.project(poly) ul_x, ul_y = map(int, pixel_poly.min(0)) lr_x, lr_y = map(int, pixel_poly.max(0)) min_x, min_y, z = poly.min(0) max_x, max_y, z = poly.max(0) ul_x = max(0, ul_x) ul_y = max(0, ul_y) lr_x = min(src_image.RasterXSize - 1, lr_x) lr_y = min(src_image.RasterYSize - 1, lr_y) samp_off = rpc_md['SAMP_OFF'] samp_off = float(samp_off) - ul_x rpc_md['SAMP_OFF'] = str(samp_off) line_off = rpc_md['LINE_OFF'] line_off = float(line_off) - ul_y rpc_md['LINE_OFF'] = str(line_off) model.image_offset[0] -= ul_x model.image_offset[1] -= ul_y # Calculate the pixel size of the new image # Constrain the width and height to the bounds of the image px_width = int(lr_x - ul_x + 1) if px_width + ul_x > src_image.RasterXSize - 1: px_width = int(src_image.RasterXSize - ul_x - 1) px_height = int(lr_y - ul_y + 1) if px_height + ul_y > src_image.RasterYSize - 1: px_height = int(src_image.RasterYSize - ul_y - 1) # We've constrained x & y so they are within the image. # If the width or height ends up negative at this point, # the AOI is completely outside the image if px_width < 0 or px_height < 0: print('AOI out of range, skipping\n') continue corners = [[0, 0], [px_width, 0], [px_width, px_height], [0, px_height]] corner_names = ['UpperLeft', 'UpperRight', 'LowerRight', 'LowerLeft'] world_corners = model.back_project(corners, elevation) corner_gcps = [] for (p, l), (x, y, h), n in zip(corners, world_corners, corner_names): corner_gcps.append(gdal.GCP(x, y, h, p, l, "", n)) # Load the source data as a gdalnumeric array clip = src_image.ReadAsArray(ul_x, ul_y, px_width, px_height) # create output raster raster_band = src_image.GetRasterBand(1) output_driver = gdal.GetDriverByName('MEM') # In the event we have multispectral images, # shift the shape dimesions we are after, # since position 0 will be the number of bands try: clip_shp_0 = clip.shape[0] clip_shp_1 = clip.shape[1] if clip.ndim > 2: clip_shp_0 = clip.shape[1] clip_shp_1 = clip.shape[2] except (AttributeError): print('Error decoding image, skipping\n') continue output_dataset = output_driver.Create('', clip_shp_1, clip_shp_0, src_image.RasterCount, raster_band.DataType) # Copy All metadata data from src to dst domains = src_image.GetMetadataDomainList() for tag in domains: md = src_image.GetMetadata(tag) if md: output_dataset.SetMetadata(md, tag) # Rewrite the rpc_md that we modified above. output_dataset.SetMetadata(rpc_md, 'RPC') output_dataset.SetGeoTransform(gdal.GCPsToGeoTransform(corner_gcps)) output_dataset.SetProjection(gdal_get_projection(src_image)) # End logging, print blank line for clarity print('') bands = src_image.RasterCount if bands > 1: for i in range(bands): outBand = output_dataset.GetRasterBand(i + 1) outBand.SetNoDataValue(nodata_values[i]) outBand.WriteArray(clip[i]) else: outBand = output_dataset.GetRasterBand(1) outBand.SetNoDataValue(nodata_values[0]) outBand.WriteArray(clip) if dst_img_file: output_driver = gdal.GetDriverByName('GTiff') output_driver.CreateCopy(dst_img_file, output_dataset, False)
def test_rpc_back_projection(): model = rpc_from_gdal_dict(rpc_md) img_pt = model.project(points[0]) bp = model.back_project(img_pt, points[0][2]) print("diff: ", bp - points[0]) assert numpy.max(numpy.abs(bp - points[0])) < 1e-16
def test_rpc_multi_projection(): model = rpc_from_gdal_dict(rpc_md) print(model.project(points))
def test_rpc_from_gdal_dict(): rpc_from_gdal_dict(rpc_md)