Newer
Older
"""
/***************************************************************************

Benjamin Jakimow
committed
EO Time Series Viewer
-------------------
begin : 2015-08-20
git sha : $Format:%H$
copyright : (C) 2017 by HU-Berlin
email : benjamin.jakimow@geo.hu-berlin.de
***************************************************************************/
/***************************************************************************
* *
* 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. *
* *
***************************************************************************/
"""
# noinspection PyPep8Naming

benjamin.jakimow@geo.hu-berlin.de
committed
import os, sys
import pickle
import datetime
from qgis.gui import *
from qgis.core import *
from eotimeseriesviewer.utils import SpatialPoint, SpatialExtent, geo2px, px2geo
from osgeo import gdal, gdal_array, osr

benjamin.jakimow@geo.hu-berlin.de
committed
if DEBUG:
logger = multiprocessing.log_to_stderr()
logger.setLevel(multiprocessing.SUBDEBUG)
def dprint(msg):
if DEBUG:
print('PixelLoader: {}'.format(msg))
def isOutOfImage(ds, px):
"""
Evaluates if a pixel is inside and image or onot
:param ds: gdal.Dataset
:param px: QPoint
:return: True | False
"""
if px.x() < 0 or px.x() >= ds.RasterXSize:
return True
if px.y() < 0 or px.y() >= ds.RasterYSize:
return True
return False
class PixelLoaderTask(object):
An object to store the loading results from a *single* raster source.

benjamin.jakimow@geo.hu-berlin.de
committed
@staticmethod
def fromDump(byte_object):
return pickle.loads(byte_object)
def __init__(self, source:str, geometries, bandIndices=None, **kwargs):

benjamin.jakimow@geo.hu-berlin.de
committed
if not isinstance(geometries, list):
geometries = [geometries]
assert isinstance(geometries, list)
geometries = [g for g in geometries if isinstance(g, (SpatialExtent, SpatialPoint))]
# assert isinstance(source, str) or isinstance(source, unicode)
self.sourcePath = source
self.geometries = geometries
self.bandIndices = bandIndices

benjamin.jakimow@geo.hu-berlin.de
committed
self.mIsDone = False
self.resCrsWkt = None
self.resGeoTransformation = None
self.resProfiles = None
self.resNoDataValues = None
self.exception = None
self.info = None

benjamin.jakimow@geo.hu-berlin.de
committed
assert isinstance(k, str)
assert not k.startswith('_')
if not k in self.__dict__.keys():
self.__dict__[k] = kwargs[k]
def setId(self, idStr:str):
self.mId = idStr
def id(self)->str:
return self.mId

benjamin.jakimow@geo.hu-berlin.de
committed
def toDump(self):
return pickle.dumps(self)
def validPixelValues(self, i):
if not self.success():
return False
if i >= len(self.resProfiles):
return False
profileData = self.resProfiles[i]
if profileData in [INFO_OUT_OF_IMAGE, INFO_NO_DATA]:
return False
else:
return True
def imageCrs(self):
return QgsCoordinateReferenceSystem(self.resCrsWkt)
def pixelResolution(self):
"""
Returns the spatial pixel resolution
:return: QSize
"""
if self.resGeoTransformation is None:
return None
p1 = px2geo(QPoint(0, 0), self.resGeoTransformation)
p2 = px2geo(QPoint(1, 1), self.resGeoTransformation)
xRes = abs(p2.x() - p1.x())
yRes = abs(p2.y() - p1.y())
return QSize(xRes, yRes)
def success(self):

Benjamin Jakimow
committed
"""
Returns True if the PixelLoaderTask has been finished well without exceptions.
:return: True | False
"""
"""
:return:
"""

benjamin.jakimow@geo.hu-berlin.de
committed
result = self.mIsDone and self.exception is None

Benjamin Jakimow
committed
return result
def __repr__(self):
info = ['PixelLoaderTask:']

benjamin.jakimow@geo.hu-berlin.de
committed
if not self.mIsDone:
info.append('not started...')
else:
if self.bandIndices:
info.append('\tBandIndices {}:{}'.format(len(self.bandIndices), self.bandIndices))
if self.resProfiles:
info.append('\tProfileData: {}'.format(len(self.resProfiles)))
for i, p in enumerate(self.resProfiles):
g = self.geometries[i]
d = self.resProfiles[i]

benjamin.jakimow@geo.hu-berlin.de
committed
if d in [INFO_OUT_OF_IMAGE, INFO_NO_DATA]:

Benjamin Jakimow
committed
info.append('\t{}: {}:{}'.format(i + 1, g, d))
else:
vData = d[0]
vStd = d[1]

benjamin.jakimow@geo.hu-berlin.de
committed
try:
info.append('\t{}: {}:{} : {}'.format(i+1, g, vData.shape, vData))
except:
s =""
return '\n'.join(info)
INFO_OUT_OF_IMAGE = 'out of image'

benjamin.jakimow@geo.hu-berlin.de
committed
INFO_NO_DATA = 'no data values'
#def loadProfiles(pathsAndBandIndices, jobid, poolWorker, geom, q, cancelEvent, **kwargs):
def transformPoint2Px(trans, pt, gt):
x, y, _ = trans.TransformPoint(pt.x(), pt.y())
return geo2px(QgsPointXY(x, y), gt)

Benjamin Jakimow
committed
def doLoaderTask(qgsTask:QgsTask, dump):

Benjamin Jakimow
committed
assert isinstance(qgsTask, QgsTask)
tasks = pickle.loads(dump)
results = []

Benjamin Jakimow
committed
nTasks = len(tasks)
qgsTask.setProgress(0)
for iTask, task in enumerate(tasks):
assert isinstance(task, PixelLoaderTask)
result = task
ds = gdal.Open(task.sourcePath, gdal.GA_ReadOnly)
nb, ns, nl = ds.RasterCount, ds.RasterXSize, ds.RasterYSize
bandIndices = list(range(nb)) if task.bandIndices is None else list(task.bandIndices)
# ensure to load valid indices only
bandIndices = [i for i in bandIndices if i >= 0 and i < nb]
task.bandIndices = bandIndices

Benjamin Jakimow
committed
gt = ds.GetGeoTransform()
result.resGeoTransformation = gt
result.resCrsWkt = ds.GetProjection()
if result.resCrsWkt == '':
result.resCrsWkt = QgsRasterLayer(ds.GetDescription()).crs().toWkt()
crsSrc = osr.SpatialReference(result.resCrsWkt)

Benjamin Jakimow
committed
# convert Geometries into pixel indices to be extracted
PX_SUBSETS = []

Benjamin Jakimow
committed
for geom in task.geometries:
crsRequest = osr.SpatialReference()

Benjamin Jakimow
committed
if geom.crs().isValid():
crsRequest.ImportFromWkt(geom.crs().toWkt())
else:
crsRequest.ImportFromWkt(crsSrc.ExportToWkt())
trans = osr.CoordinateTransformation(crsRequest, crsSrc)
if isinstance(geom, QgsPointXY):
ptUL = ptLR = QgsPointXY(geom)
elif isinstance(geom, QgsRectangle):
TYPE = 'RECTANGLE'
ptUL = QgsPointXY(geom.xMinimum(), geom.yMaximum())
ptLR = QgsPointXY(geom.xMaximum(), geom.yMinimum())
else:
raise NotImplementedError('Unsupported geometry {} {}'.format(type(geom), str(geom)))
pxUL = transformPoint2Px(trans, ptUL, gt)
pxLR = transformPoint2Px(trans, ptLR, gt)
bUL = isOutOfImage(ds, pxUL)
bLR = isOutOfImage(ds, pxLR)
if all([bUL, bLR]):
PX_SUBSETS.append(INFO_OUT_OF_IMAGE)
def shiftIntoImageBounds(pt, xMax, yMax):
assert isinstance(pt, QPoint)
if pt.x() < 0:
pt.setX(0)
elif pt.x() > xMax:
pt.setX(xMax)
if pt.y() < 0:
pt.setY(0)
elif pt.y() > yMax:
pt.setY(yMax)
shiftIntoImageBounds(pxUL, ds.RasterXSize, ds.RasterYSize)
shiftIntoImageBounds(pxLR, ds.RasterXSize, ds.RasterYSize)
if pxUL == pxLR:
size_x = size_y = 1
size_x = abs(pxUL.x() - pxLR.x())
size_y = abs(pxUL.y() - pxLR.y())
if size_x < 1:
size_x = 1
if size_y < 1:
size_y = 1
PX_SUBSETS.append((pxUL, pxUL, size_x, size_y))
PROFILE_DATA = []
if bandIndices == range(ds.RasterCount):
# we have to extract all bands
# in this case we use gdal.Dataset.ReadAsArray()
noData = ds.GetRasterBand(1).GetNoDataValue()
for px in PX_SUBSETS:
if px == INFO_OUT_OF_IMAGE:
PROFILE_DATA.append(INFO_OUT_OF_IMAGE)
pxUL, pxUL, size_x, size_y = px
bandData = ds.ReadAsArray(pxUL.x(), pxUL.y(), size_x, size_y).reshape((nb, size_x*size_y))

Benjamin Jakimow
committed
if noData:
isValid = np.ones(bandData.shape[1], dtype=np.bool)
for b in range(bandData.shape[0]):
isValid *= bandData[b, :] != ds.GetRasterBand(b+1).GetNoDataValue()
bandData = bandData[:, np.where(isValid)[0]]
PROFILE_DATA.append(bandData)
else:
# access band values band-by-band
# in this case we use gdal.Band.ReadAsArray()
# and need to iterate over the requested band indices
# save the returned band values for each geometry in a separate list
# empty list == invalid geometry
for i in range(len(PX_SUBSETS)):
if PX_SUBSETS[i] == INFO_OUT_OF_IMAGE:
PROFILE_DATA.append(INFO_OUT_OF_IMAGE)

benjamin.jakimow@geo.hu-berlin.de
committed
else:
PROFILE_DATA.append([])
for bandIndex in bandIndices:
band = ds.GetRasterBand(bandIndex+1)
noData = band.GetNoDataValue()
assert isinstance(band, gdal.Band)
for i, px in enumerate(PX_SUBSETS):
if px == INFO_OUT_OF_IMAGE:
continue
pxUL, pxUL, size_x, size_y = px
bandData = band.ReadAsArray(pxUL.x(), pxUL.y(), size_x, size_y).flatten()
if noData:
bandData = bandData[np.where(bandData != noData)[0]]
PROFILE_DATA[i].append(bandData)
for i in range(len(PX_SUBSETS)):
pd = PROFILE_DATA[i]
if isinstance(pd, list):
if len(pd) == 0:
PROFILE_DATA[i] = INFO_OUT_OF_IMAGE
else:
#PROFILE_DATA[i] = np.dstack(pd).transpose(2,0,1)
PROFILE_DATA[i] = np.vstack(pd)
# finally, ensure that there is on 2D array only
for i in range(len(PROFILE_DATA)):
d = PROFILE_DATA[i]
if isinstance(d, np.ndarray):
assert d.ndim == 2
b, yx = d.shape
assert b == len(bandIndices)
_, _, size_x, size_y = PX_SUBSETS[i]

benjamin.jakimow@geo.hu-berlin.de
committed
if yx > 0:
d = d.reshape((b, yx))
vMean = d.mean(axis=1)
vStd = d.std(axis=1)
assert len(vMean) == len(bandIndices)
assert len(vStd) == len(bandIndices)
PROFILE_DATA[i] = (vMean, vStd)
else:
PROFILE_DATA[i] = INFO_NO_DATA

benjamin.jakimow@geo.hu-berlin.de
committed
s = ""
task.resProfiles = PROFILE_DATA
task.mIsDone = True
results.append(task)

Benjamin Jakimow
committed
qgsTask.setProgress(100 * (iTask + 1) / nTasks)

benjamin.jakimow@geo.hu-berlin.de
committed
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
class LoadingProgress(object):
def __init__(self, id, nFiles):
assert isinstance(nFiles, int)
assert isinstance(id, int)
self.mID = id
self.mSuccess = 0
self.mTotal = nFiles
self.mFailed = 0
def addResult(self, success=True):
assert self.done() <= self.mTotal
if success:
self.mSuccess += 1
else:
self.mFailed += 1
def id(self):
return self.mID
def failed(self):
return self.mFailed
def done(self):
return self.mSuccess + self.mFailed
def total(self):
return self.mTotal
class PixelLoader(QObject):
"""
Loads pixel from raster images
Use QgsTaskManager interface in background

Benjamin Jakimow
committed
sigProgressChanged = pyqtSignal(float)
def __init__(self, *args, **kwds):
super(PixelLoader, self).__init__(*args, **kwds)
def tasks(self)->list:
"""
Returns the list of QgsTaskWrappers
:return: list
"""
def taskManager(self)->QgsTaskManager:
return QgsApplication.taskManager()
def startLoading(self, tasks):
assert isinstance(tasks, list)

Benjamin Jakimow
committed
self.sigLoadingStarted.emit()

Benjamin Jakimow
committed
qgsTask = QgsTask.fromFunction('Load band values', doLoaderTask, dump, on_finished=self.onLoadingFinished)
assert isinstance(qgsTask, QgsTask)

Benjamin Jakimow
committed
tid = id(qgsTask)

Benjamin Jakimow
committed
qgsTask.taskCompleted.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
qgsTask.taskTerminated.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
qgsTask.progressChanged.connect(self.sigProgressChanged.emit)
self.mTasks[tid] = qgsTask
tm = self.taskManager()
tm.addTask(qgsTask)
def onRemoveTask(self, tid):
if tid in self.mTasks.keys():
del self.mTasks[tid]
error = args[0]
if error is None:
dump = args[1]
tasks = pickle.loads(dump)
for plt in tasks:
assert isinstance(plt, PixelLoaderTask)
#self.mTasks.pop(plt.id())

Benjamin Jakimow
committed
self.sigLoadingFinished.emit()
def cancelLoading(self):
raise NotImplementedError