예제 #1
0
    def __init__(self, gt, cs, img):
        # Set Image Metadata
        self.img = img
        self.gt = gt
        self.x_size = self.img.shape[1]
        self.y_size = self.img.shape[0]

        # Set up the coordinate systems and transformation objects to/from
        # wgs84
        self.cs = cs
        self.m_per_pix = self.cs.GetLinearUnits()
        if not self.cs.IsGeographic():
            self.m_per_pix = self.m_per_pix * self.gt[1]
        self.m_north_south = self.y_size * self.m_per_pix
        self.m_east_west = self.x_size * self.m_per_pix
        new_cs_wkt = osr.GetWellKnownGeogCSAsWKT('wgs84')
        self.tf_to_wgs84 = georeg.coord_transform_from_wkt(
            self.cs.ExportToWkt(), new_cs_wkt)
        self.tf_wgs_to_geo = georeg.coord_transform_from_wkt(
            new_cs_wkt, self.cs.ExportToWkt())

        c_g = np.zeros((4, 2))
        # Upper left, upper right, lower left, lower right
        # Image frame = +x right, +y down... Numpy = +x down, +y right
        c_g[0, :] = georeg.geo_from_pix(np.array([0, 0]), self.gt)
        c_g[1, :] = georeg.geo_from_pix(np.array([self.x_size, 0]), self.gt)
        c_g[2, :] = georeg.geo_from_pix(np.array([0, self.y_size]), self.gt)
        c_g[3, :] = georeg.geo_from_pix(np.array([self.x_size, self.y_size]),
                                        self.gt)
        self.corners_geo = c_g
        self.corners_wgs = np.array(
            self.tf_to_wgs84.TransformPoints(c_g))[:, 0:2]
예제 #2
0
    def check_hdf_metadata(self):
        """
        This function makes sure that the HDF5 file has a valid image, and
        geometric coordinate system data, and appropriate transformation data
        """
        # Check to see if the image is stored
        try:
            self.img = self.hdf[self.img_path]
        except KeyError:
            print('Could not find Image at: %s', self.img_path)
        self.x_size = self.img.shape[1]
        self.y_size = self.img.shape[0]
        self.shape = self.img.shape
        # if len(self.img.shape) > 2:
        #     raise ValueError('Pointing to Image path with > 2 Dimensions')

        # Now grab the GeoRegistration data, both the geo transform and the
        # CS Wkt stored as attributes in /ophoto
        self.gt = self.hdf['/ophoto'].attrs['upper_left_corner_geo_transform']
        self.cs = osr.SpatialReference(
            str(self.hdf['/ophoto'].attrs['coordinate_system']))
        if self.cs.GetAttrValue('AUTHORITY', 1) is not None:
            self.epsg = self.cs.GetAttrValue('AUTHORITY', 1)
            crs = rasterio.crs.CRS.from_epsg(self.epsg)
            self.cs = osr.SpatialReference(crs.wkt)
        if self.cs.IsProjected():
            self.m_per_pix = self.cs.GetLinearUnits()
        else:
            self.m_per_pix = 1.0
        if not self.cs.IsGeographic():
            self.m_per_pix = self.m_per_pix * self.gt[1]

        # Set Corner Lat-Lon
        new_cs_wkt = osr.GetWellKnownGeogCSAsWKT('wgs84')
        self.tf_to_wgs84 = georeg.coord_transform_from_wkt(
            self.cs.ExportToWkt(), new_cs_wkt)
        self.tf_wgs_to_geo = georeg.coord_transform_from_wkt(
            new_cs_wkt, self.cs.ExportToWkt())

        c_g = np.zeros((4, 2))
        # Upper left, upper right, lower left, lower right
        # Image frame = +x right, +y down... Numpy = +x down, +y right
        c_g[0, :] = georeg.geo_from_pix(np.array([0, 0]), self.gt)
        c_g[1, :] = georeg.geo_from_pix(np.array([self.x_size, 0]), self.gt)
        c_g[2, :] = georeg.geo_from_pix(np.array([0, self.y_size]), self.gt)
        c_g[3, :] = georeg.geo_from_pix(np.array([self.x_size, self.y_size]),
                                        self.gt)
        self.corners_geo = c_g
        try:
            self.corners_wgs = np.array(
                self.tf_to_wgs84.TransformPoints(c_g))[:, 0:2]
        except:
            self.hdf.close()
            raise ValueError("HDF5 WKT for reference system was most \
                              likely inusfficent to perform transform")
예제 #3
0
    def __init__(self, vrt_file, **kwargs):
        self.is_closed = False
        self.vrt = rasterio.open(vrt_file, 'r')
        self.x_size = self.vrt.width
        self.y_size = self.vrt.height
        self.shape = self.vrt.shape
        self.num_bands = self.vrt.meta['count']

        base_cs = self.vrt.crs
        osr_ref = osr.SpatialReference()
        osr_ref.ImportFromProj4(rasterio.crs.CRS.to_string(base_cs))
        base_wkt = osr_ref.ExportToWkt()

        # Now grab the GeoRegistration data, both the geo transform and the
        # CS Wkt stored as attributes in /ophoto
        self.gt = self.vrt.get_transform()
        self.cs = osr.SpatialReference(yaml.dump(base_wkt))
        if self.cs.IsProjected():
            self.m_per_pix = self.cs.GetLinearUnits()
        else:
            self.m_per_pix = None

        if not self.cs.IsGeographic():
            self.m_per_pix = self.m_per_pix * self.gt[1]

        # Set Corner Lat-Lon
        new_cs_wkt = osr.GetWellKnownGeogCSAsWKT('wgs84')
        self.tf_to_wgs84 = georeg.coord_transform_from_wkt(
            self.cs.ExportToWkt(), new_cs_wkt)
        self.tf_wgs_to_geo = georeg.coord_transform_from_wkt(
            new_cs_wkt, self.cs.ExportToWkt())

        c_g = np.zeros((4, 2))
        # Upper left, upper right, lower left, lower right
        # Image frame = +x right, +y down... Numpy = +x down, +y right
        c_g[0, :] = georeg.geo_from_pix(np.array([0, 0]), self.gt)
        c_g[1, :] = georeg.geo_from_pix(np.array([self.x_size, 0]), self.gt)
        c_g[2, :] = georeg.geo_from_pix(np.array([0, self.y_size]), self.gt)
        c_g[3, :] = georeg.geo_from_pix(np.array([self.x_size, self.y_size]),
                                        self.gt)
        self.corners_geo = c_g
        try:
            self.corners_wgs = np.array(
                self.tf_to_wgs84.TransformPoints(c_g))[:, 0:2]
        except:
            self.hdf.close()
            raise ValueError("HDF5 WKT for reference system was most \
                              likely inusfficent to perform transform")
예제 #4
0
def write_geotiff(filename, nc_vname, data, geotransform, projection,
                  metadata):
    # get the size of the rasters and number of rasters to write out
    if data.ndim == 2:
        Xsize = data.shape[1]
        Ysize = data.shape[0]
        nR = 1
    elif data.ndim == 3:
        Xsize = data.shape[2]
        Ysize = data.shape[1]
        nR = data.shape[0]
    else:
        raise Exception(
            "Data does not have correct number of dimensions (2 or 3)")

    # write out the GeoTIFF
    driver = gdal.GetDriverByName("GTiff")
    dst_ds = driver.Create(filename, Xsize, Ysize, nR, gdal.GDT_Byte,
                           ["COMPRESS=LZW", "INTERLEAVE=BAND"])

    # copy most of the information from the netCDF file
    dst_ds.SetGeoTransform(geotransform)

    # set a projection - WGS 84 if a projection isn't given
    if projection == "":
        proj = osr.GetWellKnownGeogCSAsWKT("WGS84")
        dst_ds.SetProjection(proj)
    else:
        dst_ds.SetProjection(projection)

    # set the color table
    rb = dst_ds.GetRasterBand(1)
    rb.WriteArray(data)

    # set the nodata value
    rb.SetNoDataValue(255)

    # only 2D data can have a color table
    if data.ndim == 2:
        # create the color table for the variable
        ct = load_color_table(nc_vname)
        rb.SetColorTable(ct)

    # set the metadata
    dst_ds.SetMetadata(metadata)
    dst_ds = None
예제 #5
0
def main():
    first_time = time.time()
    ### Read input arguments #####
    # parser = OptionParser()
    usage = "usage: %prog [options]"
    parser = OptionParser(usage=usage)
    parser.add_option('-q', '--quiet',
                      dest='verbose', default=True, action='store_false',
                      help='do not print status messages to stdout')
    parser.add_option('-i', '--ini', dest='inifile',
                      default='coastal_inun.ini', nargs=1,
                      help='ini configuration file')
    parser.add_option('-b', '--boundary',
                      nargs=1, dest='boundary',
                      help='boundary conditions file (NetCDF point time series file')
    parser.add_option('-v', '--boundary_variable',
                      nargs=1, dest='boundary_variable',
                      default='waterlevel',
                      help='variable name of boundary conditions')
    parser.add_option('-s', '--sea_level_rise',
                      dest='sea_level_rise_map', default='',
                      help='Sea level rise map (GeoTIFF)')
    parser.add_option('-g', '--subsidence',
                      dest='subsidence_map', default='',
                      help='Subsidence map (GeoTIFF)')
    parser.add_option('-d', '--destination',
                      dest='destination', default='',
                      help='Destination file')
    parser.add_option('-t', '--time',
                      dest='time', default='2010-01-01 00:00:00',
                      help='Time stamp of flood condition')
    parser.add_option('-x', '--test',
                      dest='test', default=None,
                      help='test specific tile number; report intermediate outputs')
    # for testing: tile_settings, tempdir, resistance and dist_method options as command line options.
    # if not set, these options are read from ini (default)
    parser.add_option('-y', '--tile_settings',
                      dest='tiles', default=None,
                      help='filename of JSON tile settings')
    parser.add_option('-m', '--dist_method',
                      dest='dist_method', default=None,
                      help="calculate distance along 'ldd' or use the 'eucledian' distance ")
    parser.add_option('-r', '--resistance',
                      dest='resistance', default=None,
                      help="decrease in water level as function of distance from coast [m/m]")
    parser.add_option('--zrw',
                      dest='zrw', default=None,
                      help="zero resistance water percentage thresho")
    parser.add_option('-z', '--tempdir',
                      dest='tempdir', default=None,
                      help="output directory for temporary data")
    parser.add_option('-w', '--nworkers',
                      dest='nworkers', default=cpu_count()-1,
                      help="number of parallel workers; if 1 it runs sequential") 
    (options, args) = parser.parse_args()

    if not os.path.exists(options.inifile):
        print('path to ini file cannot be found: {:s}'.format(options.inifile))
        sys.exit(1)

    # file names and directory bookkeeping
    options.destination = os.path.abspath(options.destination)
    options.dest_path = os.path.split(options.destination)[0]
    logfilename = options.destination[:-3] + '.log'

    # create dir if not exist
    if not os.path.isdir(options.dest_path):
        os.makedirs(options.dest_path)
    # delete old destination and log files
    else:
        if os.path.isfile(options.destination):
            os.unlink(options.destination)
        if os.path.isfile(logfilename):
            os.unlink(logfilename)

    # set up the logger
    logger, ch = cl.setlogger(logfilename, 'COASTAL_INUN', options.verbose)
    logger.info('$Id: coastal_inun.py 528 2018-06-19 08:41:05Z eilan_dk $')

    ### READ CONFIG FILE
    # open config-file
    config = cl.open_conf(options.inifile)

    # read settings

    options.dem_file = os.path.abspath(cl.configget(config, 'maps', 'dem_file', True))
    options.ldd_file = cl.configget(config, 'maps', 'ldd_file', None)
    if options.ldd_file is not None:
        options.ldd_file = os.path.abspath(options.ldd_file)
    options.water_perc_file = cl.configget(config, 'maps', 'water_perc_file', None)
    if options.water_perc_file is not None:
        options.water_perc_file = os.path.abspath(options.water_perc_file)
    options.egm_file = os.path.abspath(cl.configget(config, 'maps', 'egm_file', ''))
    options.x_var = cl.configget(config, 'boundary', 'x_var', 'station_x_coordinate')
    options.y_var = cl.configget(config, 'boundary', 'y_var', 'station_y_coordinate')
    options.x_tile = cl.configget(config, 'tiling', 'x_tile', 600, datatype='int')
    options.y_tile = cl.configget(config, 'tiling', 'y_tile', 600, datatype='int')
    options.x_overlap = cl.configget(config, 'tiling', 'x_overlap', 60, datatype='int')
    options.y_overlap = cl.configget(config, 'tiling', 'y_overlap', 60, datatype='int')
    if options.tiles is None:
        options.tiles = cl.configget(config, 'tiling', 'tiles', None)
        if options.tiles is not None:
            options.tiles = os.path.abspath(options.tiles)
    if options.resistance is None:
        options.resistance = cl.configget(config, 'flood_routine', 'resistance', 0.00050, datatype='float')
    else:
        options.resistance = float(options.resistance)
    if options.zrw is None:
        options.zrw = cl.configget(config, 'flood_routine', 'waterp_thresh', 1.0, datatype='float')
    else:
        options.zrw = float(options.zrw)
    if options.dist_method is None:
        options.dist_method = cl.configget(config, 'flood_routine', 'dist_method', 'eucledian')
    if options.tempdir is None:
        options.tempdir = os.path.abspath(cl.configget(config, 'flood_routine', 'tempdir', os.path.join(options.dest_path, 'temp{:s}'.format(str(uuid.uuid4())))))
    options.nodatavalue = cl.configget(config, 'flood_routine', 'nodatavalue', -9999, datatype='float')
    options.srs = osr.GetWellKnownGeogCSAsWKT(cl.configget(config, 'flood_routine', 'srs', 'EPSG:4326'))

    # required input
    if not options.destination:   # if destination is not given
        parser.error('destination not given')
    if not options.boundary:   # if boundary conditions argument is not given
        #options.boundary='global_etc_rp_database.nc'
        parser.error('boundary conditions not given')
    if not os.path.exists(options.dem_file):
        logger.error('path to dem file {:s} cannot be found'.format(options.dem_file))
        sys.exit(1)
    if options.dist_method not in ['ldd', 'eucledian']:
        logger.error("unknown value for distance method use 'ldd' or 'eucledian'")
        sys.exit(1)

    # check paths and set default to None if not given
    options.ldd_file = cl.check_input_fn(options.ldd_file, logger)
    options.egm_file = cl.check_input_fn(options.egm_file, logger)
    try:
        options.sea_level_rise_map = float(options.sea_level_rise_map) #constant SLR
    except:
        options.sea_level_rise_map = cl.check_input_fn(options.sea_level_rise_map, logger)
    options.subsidence_map = cl.check_input_fn(options.subsidence_map, logger)
    options.water_perc_file = cl.check_input_fn(options.water_perc_file, logger)

    # make sure tempdir is new empty folder
    if not os.path.isdir(options.tempdir):
        os.makedirs(options.tempdir)
    elif os.listdir(options.tempdir) == "":
        options.tempdir = options.tempdir  # do nothing
    else:
        n = 1
        options.tempdir = options.tempdir + '_{:03d}'.format(n)
        while os.path.isdir(options.tempdir):
            n += 1
            options.tempdir = options.tempdir[:-4] + '_{:03d}'.format(n)
        os.makedirs(options.tempdir)

    # write info to logger
    logger.info('Destination file: {:s}'.format(options.destination))
    logger.info('Temporary directory: {:s}'.format(options.tempdir))
    logger.info('Time of flood conditions: {:s}'.format(options.time))
    logger.info('DEM file: {:s}'.format(options.dem_file))
    logger.info('LDD file: {:s}'.format(options.ldd_file))
    logger.info('EGM file: {:s}'.format(options.egm_file))
    logger.info('Water mask file: {:s}'.format(options.water_perc_file))
    logger.info('Sea level rise map: {}'.format(options.sea_level_rise_map))
    logger.info('Subsidence map: {:s}'.format(options.subsidence_map))
    logger.info('Using tiling from json file: {:s}'.format(str(options.tiles is not None)))
    if options.tiles is None:
        logger.info('Columns per tile: {:d}'.format(options.x_tile))
        logger.info('Rows per tile: {:d}'.format(options.y_tile))
        logger.info('Columns overlap: {:d}'.format(options.x_overlap))
        logger.info('Rows overlap: {:d}'.format(options.y_overlap))
    logger.info('Flood resistance: {:.6f}'.format(options.resistance))
    logger.info('Distance method: {:s}'.format(options.dist_method))
    if options.test is None:
        logger.info('Running test: False')
    else:
        logger.info('Running test: True')

    #########################################################################
    # PREPARE TILES AND OUTPUT
    #########################################################################

    # add metadata from the section [metadata]
    metadata_global = {}
    meta_keys = config.options('metadata')
    for key in meta_keys:
        metadata_global[key] = config.get('metadata', key)
    # add a number of metadata variables that are mandatory
    metadata_global['config_file'] = os.path.abspath(options.inifile)
    metadata_var = {}
    metadata_var['units'] = 'm'
    metadata_var['standard_name'] = 'water_surface_height_above_reference_datum'
    metadata_var['long_name'] = 'Coastal flooding'
    metadata_var['comment'] = 'water_surface_reference_datum_altitude is given in file {:s}'.format(options.dem_file)

    # copy inifile to tempdir for reproducibility
    inifilename = options.destination[:-3] + '.ini'
    copyfile(options.inifile, inifilename)

    # Read extent from a GDAL compatible file
    try:
        x, y = cl.get_gdal_axes(options.dem_file, logging=logger)
    except:
        msg = 'Input file {:s} not a gdal compatible file'.format(options.dem_file)
        cl.close_with_error(logger, ch, msg)
        sys.exit(1)

    # open the variable with boundary conditions as preparation to read array parts
    try:
        with nc.Dataset(options.boundary, 'r') as a:
            # read history from boundary conditions
            try:
                history = a.history
            except:
                history = 'not provided'
            metadata_global['history'] = """Created by: $Id: coastal_inun.py 528 2018-06-19 08:41:05Z eilan_dk $,
                                            boundary conditions from {:s},\nhistory: {:s}""".format(
                os.path.abspath(options.boundary), history)
    except:
        msg = 'Input file {:s} not a netcdf compatible file'.format(options.boundary)
        cl.close_with_error(logger, ch, msg)
        sys.exit(1)

    # first -setup a NetCDF file
    nc_funcs.prepare_nc(options.destination, x, np.flipud(y),
                        [datetime.datetime.strptime(options.time, '%Y-%m-%d %H:%M:%S')],
                        metadata_global, units='Days since 1960-01-01 00:00:00')
    nc_funcs.append_nc(options.destination, 'inun', chunksizes=(1, min(options.y_tile, len(y)), min(options.x_tile, len(x))),
                       fill_value=options.nodatavalue, metadata=metadata_var)

    # read tile settings is json file given
    if options.tiles is not None:
        with open(options.tiles, 'r') as data_file:
            tile_list = json.load(data_file)
        logger.info('raster tile setting read from {:s}'.format(options.tiles))
    #  otherwise, start discretizing
    else:
        logger.info('discretizing raster...')
        tile_list = cl.discretize_raster_bounds(options.dem_file,
                                                options.x_tile, options.y_tile, options.x_overlap, options.y_overlap,
                                                options.boundary, options.x_var, options.y_var)
        # write output to tempdir
        tiles_fn = options.destination[:-3] + '_tiles.json'
        with open(tiles_fn, 'w') as outfile:
            json.dump(tile_list, outfile)

    # add options to tiles list
    tile_list_out = []
    for t in tile_list:
        t['options'] = options
        t['fn'] = os.path.join(options.tempdir, 'flood_{:05d}.tif'.format(t['i']))
        tile_list_out.append(t)
    tile_list = tile_list_out

    # discretizing finished
    n_tiles = len(tile_list)
    logger.info('raster discretized in {:d} tiles'.format(n_tiles))

    # find tiles with SRTM data and boundary conditions
    flood_tiles = [i for i, tile in enumerate(tile_list) if tile['has_DEM_data'] and tile['has_bounds']]
    logger.info('run {:d} tiles with SRTM data and boundary conditions'.format(len(flood_tiles)))

    # test with random subset of tiles
    if options.test is not None:
        tile_list = [tile for tile in tile_list if tile['i'] in [int(options.test)]]
        n_tiles = len(tile_list)
        logger.info('test with subset of {:d} tiles'.format(n_tiles))

    #########################################################################
    # PROCESS TILES
    #########################################################################

    # initialize multiprocessing parameters
    nworkers = np.min([int(options.nworkers), n_tiles])   # number of cores to work with
    logger.info('start processing tiles with {:d} parallel processes'.format(nworkers))
    time0 = time.time()

    # process tiles multicore
    if nworkers > 1:
       p = Pool(nworkers)
       try:
           p.map(process_tile, tile_list)
       except:
           traceback.print_exc()
       p.close()
    else:
       [process_tile(t) for t in tile_list] #[process_tile(t) for t in tile_list] 

    # ##############################################
    seconds = float(time.time()-time0)
    hours, seconds = seconds // 3600, seconds % 3600
    minutes, seconds = seconds // 60, seconds % 60
    logger.info('finished processing {:d} tiles in {:02d}:{:02d}:{:02d}'.format(
        n_tiles, int(hours), int(minutes), int(seconds)))

    #########################################################################
    # WRITE DATA TO NETCDF
    #########################################################################
    # open the prepared netCDF file for appending
    nc_obj = nc.Dataset(options.destination, 'a')
    nc_var = nc_obj.variables['inun']
    logger.info('start appending data to netcdf file')
    time0 = time.time()

    # read data from tempdir and append
    for tile in tile_list:
        # read data
        _, _, flood_cut, fill_val = gdal_readmap(tile['fn'], 'GTiff')
        flood_cut = np.flipud(flood_cut)
        if tile['start_row'] == 0:
            nc_var[0, -tile['end_row']:, tile['start_col']:tile['end_col']] = flood_cut
        else:
            nc_var[0, -tile['end_row']:-tile['start_row'], tile['start_col']:tile['end_col']] = flood_cut

    # now close nc file
    nc_obj.sync()
    nc_obj.close()

    seconds = float(time.time()-time0)
    hours, seconds = seconds // 3600, seconds % 3600
    minutes, seconds = seconds // 60, seconds % 60
    logger.info('finished writing {:d} tiles in {:02d}:{:02d}:{:02d}'.format(
        n_tiles, int(hours), int(minutes), int(seconds)))

    # cleanup temp dir
    if options.test is None:
        cl.cleanDir(options.tempdir, logger)

    # log total processing time
    seconds = float(time.time()-first_time)
    hours, seconds = seconds // 3600, seconds % 3600
    minutes, seconds = seconds // 60, seconds % 60
    logger.info('total processing time {:02d}:{:02d}:{:02d}'.format(
        int(hours), int(minutes), int(seconds)))

    # close logger
    logger, ch = cl.closeLogger(logger, ch)
    del logger, ch
    sys.exit(0)
예제 #6
0
    def _get_gdal_metadata(self, filename):
        # let's do GDAL here ? if it fails do Hachoir
        from osgeo import gdal, osr, gdalconst
        ds = gdal.Open(filename, gdal.GA_ReadOnly)

        # TODO: get bounding box
        geotransform = ds.GetGeoTransform()
        projref = ds.GetProjectionRef()
        if not projref:
            # default to WGS84
            projref = osr.GetWellKnownGeogCSAsWKT('EPSG:4326')
        spref = osr.SpatialReference(projref)  # SRS
        # extract bbox
        #       see http://svn.osgeo.org/gdal/trunk/gdal/swig/python/samples/gdalinfo.py
        #       GDALInfoReportCorner
        # bbox = left,bottom,right,top
        # bbox = min Longitude , min Latitude , max Longitude , max Latitude
        # bbox in srs units
        # transform points into georeferenced coordinates
        left, top = self._geotransform(0.0, 0.0, geotransform)
        right, bottom = self._geotransform(ds.RasterXSize, ds.RasterYSize,
                                           geotransform)
        # transform points to dataset projection coordinates
        if not spref.IsLocal():
            # TODO: check whether it really is not possible to transform local coordinate systems
            spref_latlon = spref.CloneGeogCS()
            trans = osr.CoordinateTransformation(spref_latlon, spref)
            left, top, _ = trans.TransformPoint(left, top, 0)
            right, bottom, _ = trans.TransformPoint(right, bottom, 0)
            srs = '{0}:{1}'.format(
                spref.GetAuthorityName(
                    None),  # 'PROJCS', 'GEOGCS', 'GEOGCS|UNIT', None
                spref.GetAuthorityCode(None))
        else:
            srs = None
        # build metadata struct
        data = {
            'size': (ds.RasterXSize, ds.RasterYSize),
            'bands': ds.RasterCount,
            'projection': projref,  # WKT
            'srs': srs,
            'origin': (geotransform[0], geotransform[3]),
            'Pxiel Size': (geotransform[1], geotransform[5]),
            'bounds': {
                'left': left,
                'bottom': bottom,
                'right': right,
                'top': top
            }
        }
        data.update(ds.GetMetadata_Dict())
        data.update(ds.GetMetadata_Dict('EXIF'))
        from libxmp.core import XMPMeta
        xmp = ds.GetMetadata('xml:XMP') or []
        if xmp:
            data['xmp'] = {}
        for xmpentry in xmp:
            xmpmd = XMPMeta()
            xmpmd.parse_from_str(xmpentry)
            for xmpitem in self._traverseXMP(xmpmd):
                (schema, name, value, options) = xmpitem
                if options['IS_SCHEMA']:
                    continue
                if options['ARRAY_IS_ALT']:
                    # pick first element and move on
                    data['xmp'][name] = xmpmd.get_array_item(schema, name, 1)
                    continue
                # current item

                # ARRAY_IS_ALT .. ARRAY_IS_ALT_TEXT, pick first one (value is array + array is ordered)

                # -> array elements don't have special markers :(

                if options['ARRAY_IS_ALT']:
                    pass
                if options['HAS_LANG']:
                    pass
                if options['VALUE_IS_STRUCT']:
                    pass
                if options['ARRAY_IS_ALTTEXT']:
                    pass
                if options['VALUE_IS_ARRAY']:
                    pass
                if options['ARRAY_IS_ORDERED']:
                    pass

                #     -> ALT ARRAY_VALUE???
                # if options['VALUE_IS_ARRAY']:

                # else:
                data['xmp'][name] = value

        # EXIF could provide at least:
        #   width, height, bistpersample, compression, planarconfiguration,
        #   sampleformat, xmp-metadata (already parsed)

        # TODO: get driver metadata?
        #     ds.GetDriver().getMetadata()
        #     ds.GetDriver().ds.GetMetadataItem(gdal.DMD_XXX)

        # Extract GDAL metadata
        for numband in range(1, ds.RasterCount + 1):
            band = ds.GetRasterBand(numband)
            (min_, max_, mean, stddev) = band.ComputeStatistics(False)
            banddata = {
                'data type':
                gdal.GetDataTypeName(band.DataType),
                # band.GetRasterColorTable().GetCount() ... color table with
                # count entries
                'min':
                min_,
                'max':
                max_,
                'mean':
                mean,
                'stddev':
                stddev,
                'color interpretation':
                gdal.GetColorInterpretationName(band.GetColorInterpretation()),
                'description':
                band.GetDescription(),
                'nodata':
                band.GetNoDataValue(),
                'size': (band.XSize, band.YSize),
                'index':
                band.GetBand(),
                #band.GetCategoryNames(), GetRasterCategoryNames() .. ?
                #band.GetScale()
            }
            banddata.update(band.GetMetadata())
            if not 'band' in data:
                data['band'] = []
            data['band'].append(banddata)

            # extract Raster Attribute table (if any)
            rat = band.GetDefaultRAT()

            def _getColValue(rat, row, col):
                valtype = rat.GetTypeOfCol(col)
                if valtype == gdalconst.GFT_Integer:
                    return rat.GetValueAsInt(row, col)
                if valtype == gdalconst.GFT_Real:
                    return rat.GetValueAsDouble(row, col)
                if valtype == gdalconst.GFT_String:
                    return rat.GetValueAsString(row, col)
                return None

            GFU_MAP = {
                gdalconst.GFU_Generic: 'Generic',
                gdalconst.GFU_Max: 'Max',
                gdalconst.GFU_MaxCount: 'MaxCount',
                gdalconst.GFU_Min: 'Min',
                gdalconst.GFU_MinMax: 'MinMax',
                gdalconst.GFU_Name: 'Name',
                gdalconst.GFU_PixelCount: 'PixelCount',
                gdalconst.GFU_Red: 'Red',
                gdalconst.GFU_Green: 'Green',
                gdalconst.GFU_Blue: 'Blue',
                gdalconst.GFU_Alpha: 'Alpha',
                gdalconst.GFU_RedMin: 'RedMin',
                gdalconst.GFU_GreenMin: 'GreenMin',
                gdalconst.GFU_BlueMin: 'BlueMin',
                gdalconst.GFU_AlphaMax: 'AlphaMin',
                gdalconst.GFU_RedMax: 'RedMax',
                gdalconst.GFU_GreenMax: 'GreenMax',
                gdalconst.GFU_BlueMin: 'BlueMax',
                gdalconst.GFU_AlphaMax: 'AlphaMax',
            }

            GFT_MAP = {
                gdalconst.GFT_Integer: 'Integer',
                gdalconst.GFT_Real: 'Real',
                gdalconst.GFT_String: 'String',
            }

            if rat:
                banddata['rat'] = {
                    'rows': [[
                        _getColValue(rat, rowidx, colidx)
                        for colidx in range(0, rat.GetColumnCount())
                    ] for rowidx in range(0, rat.GetRowCount())],
                    'cols': [{
                        'name': rat.GetNameOfCol(idx),
                        'type': GFT_MAP[rat.GetTypeOfCol(idx)],
                        'usage': GFU_MAP[rat.GetUsageOfCol(idx)],
                        'idx': idx
                    } for idx in range(0, rat.GetColumnCount())],
                }
                # Assume if there is a RAT we have categorical data
                banddata['type'] = 'categorical'
            else:
                banddata['type'] = 'continuous'

        ds = None

        # HACHOIR Tif extractor:
        # ret = {}
        # for field in parser:
        #     if field.name.startswith('ifd'):
        #         data = {
        #           'img_height': field['img_height']['value'].value,
        #           'img_width': field['img_width']['value'].value,
        #           'bits_per_sample': field['bits_per_sample']['value'].value,
        #           'compression': field['compression']['value'].display
        #         }
        #         ret = data
        return data