Skip to content
Snippets Groups Projects
timeseries.py 18.2 KiB
Newer Older
Benjamin Jakimow's avatar
Benjamin Jakimow committed
from __future__ import absolute_import
import six, sys, os, gc, re, collections, site, inspect, time, traceback, copy
import logging
logger = logging.getLogger(__name__)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
from osgeo import gdal, ogr

from qgis import *
from qgis.core import *
from qgis.gui import *
from PyQt4.QtGui import *
from PyQt4.QtCore import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed

from osgeo import gdal, ogr, gdal_array

gdal.SetConfigOption('VRT_SHARED_SOURCE', '0') #!important. really. do not change this.

Benjamin Jakimow's avatar
Benjamin Jakimow committed
import numpy as np

from timeseriesviewer import DIR_REPO, DIR_EXAMPLES, jp
from timeseriesviewer.dateparser import parseDateFromDataSet
Benjamin Jakimow's avatar
Benjamin Jakimow committed
def transformGeometry(geom, crsSrc, crsDst, trans=None):
    if trans is None:
        assert isinstance(crsSrc, QgsCoordinateReferenceSystem)
        assert isinstance(crsDst, QgsCoordinateReferenceSystem)
        return transformGeometry(geom, None, None, trans=QgsCoordinateTransform(crsSrc, crsDst))
    else:
        assert isinstance(trans, QgsCoordinateTransform)
        return trans.transform(geom)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
METRIC_EXPONENTS = {
    "nm":-9,"um": -6, "mm":-3, "cm":-2, "dm":-1, "m": 0,"hm":2, "km":3
}
#add synonyms
METRIC_EXPONENTS['nanometers'] = METRIC_EXPONENTS['nm']
METRIC_EXPONENTS['micrometers'] = METRIC_EXPONENTS['um']
METRIC_EXPONENTS['millimeters'] = METRIC_EXPONENTS['mm']
METRIC_EXPONENTS['centimeters'] = METRIC_EXPONENTS['cm']
METRIC_EXPONENTS['decimeters'] = METRIC_EXPONENTS['dm']
METRIC_EXPONENTS['meters'] = METRIC_EXPONENTS['m']
METRIC_EXPONENTS['hectometers'] = METRIC_EXPONENTS['hm']
METRIC_EXPONENTS['kilometers'] = METRIC_EXPONENTS['km']
Benjamin Jakimow's avatar
Benjamin Jakimow committed

Benjamin Jakimow's avatar
Benjamin Jakimow committed
def convertMetricUnit(value, u1, u2):
    assert u1 in METRIC_EXPONENTS.keys()
    assert u2 in METRIC_EXPONENTS.keys()

    e1 = METRIC_EXPONENTS[u1]
    e2 = METRIC_EXPONENTS[u2]

    return value * 10**(e1-e2)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

def getDS(pathOrDataset):
    if isinstance(pathOrDataset, gdal.Dataset):
        return pathOrDataset
    else:
        ds = gdal.Open(pathOrDataset)
        assert isinstance(ds, gdal.Dataset)
        return ds
Benjamin Jakimow's avatar
Benjamin Jakimow committed

class SensorInstrument(QObject):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    sigNameChanged = pyqtSignal(str)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    LUT_Wavelengths = dict({'B':480,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                            'G':570,
                            'R':660,
                            'nIR':850,
                            'swIR':1650,
                            'swIR1':1650,
                            'swIR2':2150
                            })
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    """
    Describes a Sensor Configuration
    """
    def __init__(self, pathImg, sensor_name=None):
        super(SensorInstrument, self).__init__()
        ds = getDS(pathImg)
        self.nb, nl, ns, crs, self.px_size_x, self.px_size_y = getSpatialPropertiesFromDataset(ds)
        self.bandDataType = ds.GetRasterBand(1).DataType
        self.pathImg = ds.GetFileList()[0]
        self.bandNames = [ds.GetRasterBand(b+1).GetDescription() for b in range(self.nb)]
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        self.TS = None

        assert self.px_size_x > 0
        assert self.px_size_y > 0

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        #find wavelength
        wl, wlu = parseWavelengthFromDataSet(ds)
        if wl is None:
            self.wavelengths = None
            self.wavelengthUnits = None
        else:
            self.wavelengths = np.asarray(wl)
            self.wavelengthUnits = wlu
        self._id = '{}b{}m'.format(self.nb, self.px_size_x)
            self._id += ';'.join([str(w) for w in self.wavelengths])+wlu
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if sensor_name is None:

            sensor_name = '{}bands@{}m'.format(self.nb, self.px_size_x)
        self.setName(sensor_name)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        self.hashvalue = hash(','.join(self.bandNames))

    def id(self):
        return self._id

    def setName(self, name):
        self._name = name
        self.sigNameChanged.emit(self.name())

    def name(self):
        return self._name

    def dataType(self, p_int):
        return self.bandDataType
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def bandClosestToWavelength(self, wl, wl_unit='nm'):
        """
        Returns the band index (>=0) of the band closest to wavelength wl
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        :param wl:
        :param wl_unit:
        :return:
        """
        if not self.wavelengthsDefined():
            return None

        if wl in SensorInstrument.LUT_Wavelengths.keys():
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            wl_unit = 'nm'
            wl = SensorInstrument.LUT_Wavelengths[wl]
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        wl = float(wl)
        if self.wavelengthUnits != wl_unit:
            wl = convertMetricUnit(wl, wl_unit, self.wavelengthUnits)


Benjamin Jakimow's avatar
Benjamin Jakimow committed




    def wavelengthsDefined(self):
        return self.wavelengths is not None and \
                self.wavelengthUnits is not None

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __eq__(self, other):
        if not isinstance(other, SensorInstrument):
            return False
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return self.nb == other.nb and \
               self.px_size_x == other.px_size_x and \
               self.px_size_y == other.px_size_y

    def __hash__(self):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def __repr__(self):
        return str(self.__class__) +' ' + self.name()
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def getDescription(self):
        info = []
        info.append(self.name())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        info.append('{} Bands'.format(self.nb))
        info.append('Band\tName\tWavelength')
        for b in range(self.nb):
            if self.wavelengths is not None:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                wl = str(self.wavelengths[b])
            else:
                wl = 'unknown'
            info.append('{}\t{}\t{}'.format(b + 1, self.bandNames[b], wl))

        return '\n'.join(info)


def verifyInputImage(path, vrtInspection=''):
    if path is None or not type(path) in [str, unicode]:
        return None
    ds = gdal.Open(path)
        logger.error('{}GDAL unable to open: '.format(vrtInspection, path))
    if ds.RasterCount == 0 and len(ds.GetSubDatasets()) > 0:
        logger.error('Can not open container {}.\nPlease specify a subdataset'.format(path))
    if ds.GetDriver().ShortName == 'VRT':
        vrtInspection = 'VRT Inspection {}\n'.format(path)
        nextFiles = set(ds.GetFileList()) - set([path])
        validSrc = [verifyInputImage(p, vrtInspection=vrtInspection) for p in nextFiles]
        if not all(validSrc):
            return False

    from timeseriesviewer.dateparser import parseDateFromDataSet
    date = parseDateFromDataSet(ds)
    if date is None:
        return False

def pixel2coord(gt, x, y):
    """Returns global coordinates from pixel x, y coords"""
    """https://scriptndebug.wordpress.com/2014/11/24/latitudelongitude-of-each-pixel-using-python-and-gdal/"""
    xoff, a, b, yoff, d, e = gt
    xp = a * x + b * y + xoff
    yp = d * x + e * y + yoff
    return (xp, yp)
class TimeSeriesDatum(QObject):
    @staticmethod
    def createFromPath(path):
        """
        Creates a valid TSD or returns None if this is impossible
        :param path:
        :return:
        """
    """
    Collects all data sets related to one sensor
    """
    sigVisibilityChanged = pyqtSignal(bool)
    sigRemoveMe = pyqtSignal()
    def __init__(self, timeSeries, pathImg):
        super(TimeSeriesDatum,self).__init__()
        self.pathImg = ds.GetFileList()[0] if isinstance(pathImg, gdal.Dataset) else pathImg
        self.timeSeries = timeSeries
        self.nb, self.nl, self.ns, self.crs, px_x, px_y = getSpatialPropertiesFromDataset(ds)
        self.date = parseDateFromDataSet(ds)
        assert self.date is not None, 'Unable to find acquisition date of {}'.format(pathImg)
        from timeseriesviewer.dateparser import DOYfromDatetime64
        self.doy = DOYfromDatetime64(self.date)

        gt = ds.GetGeoTransform()

        UL = QgsPoint(*pixel2coord(gt, 0, 0))
        LR = QgsPoint(*pixel2coord(gt, self.ns, self.nl))
        from timeseriesviewer.main import SpatialExtent
        self._spatialExtent = SpatialExtent(self.crs, UL, LR)

        self.srs_wkt = str(self.crs.toWkt())

        self.mVisibility = True

    def rank(self):
        return self.timeSeries.index(self)

    def setVisibility(self, b):
        old = self.mVisibility
        self.mVisibility = b
        if old != self.mVisibility:
            self.sigVisibilityChanged.emit(b)

    def isVisible(self):
        return self.mVisibility


    def getDate(self):
        return np.datetime64(self.date)


    def getSpatialReference(self):
        return self.crs

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def spatialExtent(self):

    def __repr__(self):
        return 'TS Datum {} {}'.format(self.date, str(self.sensor))

    def __eq__(self, other):
        return self.date == other.date and self.sensor.id() == other.sensor.id()
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def __lt__(self, other):
        if self.date < other.date:
            return True
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        else:
            return self.sensor.id() < other.sensor.id()
        return hash((self.date,self.sensor.id()))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
class TimeSeries(QObject):
    sigTimeSeriesDatesAdded = pyqtSignal(list)
    sigTimeSeriesDatesRemoved = pyqtSignal(list)
    sigLoadingProgress = pyqtSignal(int, int, str)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    sigSensorAdded = pyqtSignal(SensorInstrument)
    sigSensorRemoved = pyqtSignal(SensorInstrument)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __init__(self, imageFiles=None, maskFiles=None):
        QObject.__init__(self)

        #define signals

        #fire when a new TSD is added


        #self.data = collections.OrderedDict()
        self.data = list()


        self.shape = None

        self.Sensors = collections.OrderedDict()

        self.Pool = None

        if imageFiles is not None:
            self.addFiles(imageFiles)
        if maskFiles is not None:
            self.addMasks(maskFiles)

    _sep = ';'


Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def loadFromFile(self, path, n_max=None):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        images = []
        masks = []
        with open(path, 'r') as f:
            lines = f.readlines()
            for l in lines:
                if re.match('^[ ]*[;#&]', l):
                    continue

                parts = re.split('[\n'+TimeSeries._sep+']', l)
                parts = [p for p in parts if p != '']
                images.append(parts[0])
                if len(parts) > 1:
                    masks.append(parts[1])

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if n_max:
            n_max = min([len(images), n_max])
            self.addFiles(images[0:n_max])
        else:
            self.addFiles(images)
        #self.addMasks(masks)
Benjamin Jakimow's avatar
Benjamin Jakimow committed


    def saveToFile(self, path):
        if path is None or len(path) == 0:
            return

        lines = []
        lines.append('#Time series definition file: {}'.format(np.datetime64('now').astype(str)))
        lines.append('#<image path>[;<mask path>]')
        for TSD in self.data:
Benjamin Jakimow's avatar
Benjamin Jakimow committed

            line = TSD.pathImg
            lines.append(line)

        lines = [l+'\n' for l in lines]

        print('Write {}'.format(path))
        with open(path, 'w') as f:
            f.writelines(lines)

    def getPixelSizes(self):

        r = []
        for sensor in self.Sensors.keys():
            r.append((QgsRectangle(sensor.px_size_x, sensor.px_size_y)))
        return r

        return None
    def getMaxSpatialExtent(self, crs=None):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            return None

        extent = self.data[0].spatialExtent()
        if len(self.data) > 1:
            for TSD in self.data[1:]:
                extent.combineExtentWith(TSD.spatialExtent())
Benjamin Jakimow's avatar
Benjamin Jakimow committed

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return extent
Benjamin Jakimow's avatar
Benjamin Jakimow committed


    def tsdFromPath(self, path):
        for tsd in self.data:
            if tsd.pathImg == path:
                return tsd
        return False

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def getObservationDates(self):
        return [tsd.getDate() for tsd in self.data]

    def getTSD(self, pathOfInterest):
        for tsd in self.data:
            if tsd.pathImg == pathOfInterest:
                return tsd
        return None

    def getTSDs(self, dateOfInterest=None, sensorOfInterest=None):
            tsds = [tsd for tsd in tsds if tsd.getDate() == dateOfInterest]
        if sensorOfInterest:
            tsds = [tsd for tsd in tsds if tsd.sensor == sensorOfInterest]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return tsds

    def clear(self):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def removeDates(self, TSDs):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        for TSD in TSDs:
            assert type(TSD) is TimeSeriesDatum
            self.data.remove(TSD)
            TSD.timeSeries = None
            removed.append(TSD)

            if len(self.Sensors[S]) == 0:
                self.Sensors.pop(S)
        self.sigTimeSeriesDatesRemoved.emit(removed)


    def addTimeSeriesDates(self, timeSeriesDates):
        assert isinstance(timeSeriesDates, list)
        added = list()
        for TSD in timeSeriesDates:
            try:
                sensorAdded = False
                existingSensors = list(self.Sensors.keys())
                if TSD.sensor not in existingSensors:
                    self.Sensors[TSD.sensor] = list()
                    sensorAdded = True
                else:
                    TSD.sensor = existingSensors[existingSensors.index(TSD.sensor)]

                if TSD in self.data:
                    six.print_('Time series date-time already added ({} {}). \nPlease use VRTs to mosaic images with same acquisition date-time.'.format(str(TSD), TSD.pathImg), file=sys.stderr)
                else:
                    self.Sensors[TSD.sensor].append(TSD)
                    #insert sorted
                    bisect.insort(self.data, TSD)
                    TSD.timeSeries = self
                    TSD.sigRemoveMe.connect(lambda : self.removeDates([TSD]))
                    added.append(TSD)
                if sensorAdded:
                    self.sigSensorAdded.emit(TSD.sensor)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

            except:
                exc_type, exc_value, exc_traceback = sys.exc_info()
                traceback.print_exception(exc_type, exc_value, exc_traceback, limit=2)
                six.print_('Unable to add {}'.format(file), file=sys.stderr)
                pass
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        if len(added) > 0:
            self.sigTimeSeriesDatesAdded.emit(added)
Benjamin Jakimow's avatar
Benjamin Jakimow committed


    def addFiles(self, files):
        assert isinstance(files, list)
        files = [f for f in files if f is not None]

        nMax = len(files)
        nDone = 0
        self.sigLoadingProgress.emit(0,nMax, 'Start loading {} files...'.format(nMax))

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        for i, file in enumerate(files):
            tsd = TimeSeriesDatum.createFromPath(file)
            if tsd is None:
                msg = 'Unable to add: {}'.format(os.path.basename(file))
            else:
                self.addTimeSeriesDates([tsd])
                msg = 'Added {}'.format(os.path.basename(file))
                self.sigRuntimeStats.emit({'dt_addTSD':np.datetime64('now')-t0})
            nDone += 1
            self.sigLoadingProgress.emit(nDone, nMax, msg)

Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def __len__(self):
        return len(self.data)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def __iter__(self):
        return iter(self.data)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def __getitem__(self, slice):
        return self.data[slice]

    def __delitem__(self, slice):
        self.removeDates(slice)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def __contains__(self, item):
        return item in self.data
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def __repr__(self):
        info = []
        info.append('TimeSeries:')
        l = len(self)
        info.append('  Scenes: {}'.format(l))

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return '\n'.join(info)

def getSpatialPropertiesFromDataset(ds):
    assert isinstance(ds, gdal.Dataset)

    nb = ds.RasterCount
    nl = ds.RasterYSize
    ns = ds.RasterXSize
    proj = ds.GetGeoTransform()
    px_x = float(abs(proj[1]))
    px_y = float(abs(proj[5]))

    crs = QgsCoordinateReferenceSystem(ds.GetProjection())

    return nb, nl, ns, crs, px_x, px_y








def parseWavelengthFromDataSet(ds):
    assert isinstance(ds, gdal.Dataset)
    wl = None
    wlu = None

    #see http://www.harrisgeospatial.com/docs/ENVIHeaderFiles.html for supported wavelength units
    regWLkey = re.compile('.*wavelength[_ ]*$', re.I)
    regWLUkey = re.compile('.*wavelength[_ ]*units?$', re.I)
    regNumeric = re.compile(r"([-+]?\d*\.\d+|[-+]?\d+)", re.I)
    regWLU = re.compile('((micro|nano|centi)meters)|(um|nm|mm|cm|m|GHz|MHz)', re.I)
    for domain in ds.GetMetadataDomainList():
        md = ds.GetMetadata_Dict(domain)
        for key, value in md.items():
            if wl is None and regWLkey.search(key):
                numbers = regNumeric.findall(value)
                if len(numbers) == ds.RasterCount:
                    wl = [float(n) for n in numbers]

            if wlu is None and regWLUkey.search(key):
                match = regWLU.search(value)
                if match:
                    wlu = match.group().lower()
                names = ['nanometers', 'micrometers', 'millimeters', 'centimeters', 'decimeters']
                si = ['nm', 'um', 'mm', 'cm', 'dm']
                if wlu in names:
                    wlu = si[names.index(wlu)]

    return wl, wlu
Benjamin Jakimow's avatar
Benjamin Jakimow committed



def parseWavelength(lyr):
    wl = None
    wlu = None
    assert isinstance(lyr, QgsRasterLayer)
    md = [l.split('=') for l in str(lyr.metadata()).splitlines() if 'wavelength' in l.lower()]
    #see http://www.harrisgeospatial.com/docs/ENVIHeaderFiles.html for supported wavelength units
    regWLU = re.compile('((micro|nano|centi)meters)|(um|nm|mm|cm|m|GHz|MHz)')
    for kv in md:
        key, value = kv
        key = key.lower()
        if key == 'center wavelength':
            tmp = re.findall('\d*\.\d+|\d+', value) #find floats
            if len(tmp) == 0:
                tmp = re.findall('\d+', value) #find integers
            if len(tmp) == lyr.bandCount():
                wl = [float(w) for w in tmp]

        if key == 'wavelength units':
            match = regWLU.search(value)
            if match:
                wlu = match.group()

            names = ['nanometers','micrometers','millimeters','centimeters','decimenters']
            si   = ['nm','um','mm','cm','dm']
            if wlu in names:
                wlu = si[names.index(wlu)]

    return wl, wlu



if __name__ == '__main__':

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    print convertMetricUnit(100, 'cm', 'm')
    print convertMetricUnit(1, 'm', 'um')