Commit cef65607 authored by Benjamin Jakimow's avatar Benjamin Jakimow
Browse files
parent 91e6e145
......@@ -35,7 +35,7 @@ import uuid
from osgeo import osr
from ..speclib import SpectralLibrarySettingsKey
from PyQt5.QtWidgets import *
from qgis.core import \
from qgis.core import QgsApplication, \
QgsRenderContext, QgsFeature, QgsVectorLayer, QgsMapLayer, QgsRasterLayer, \
QgsAttributeTableConfig, QgsField, QgsFields, QgsCoordinateReferenceSystem, QgsCoordinateTransform, \
QgsVectorFileWriter, QgsActionManager, QgsFeatureIterator, QgsFeatureRequest, \
......@@ -43,7 +43,7 @@ from qgis.core import \
QgsRaster, QgsDefaultValue, QgsReadWriteContext, \
QgsCategorizedSymbolRenderer, QgsMapLayerProxyModel, \
QgsSymbol, QgsNullSymbolRenderer, QgsMarkerSymbol, QgsLineSymbol, QgsFillSymbol, \
QgsEditorWidgetSetup, QgsAction, QgsTask, QgsMessageLog
QgsEditorWidgetSetup, QgsAction, QgsTask, QgsMessageLog, QgsFileUtils
from qgis.gui import \
QgsGui, QgsMapCanvas, QgsDualView, QgisInterface, QgsEditorConfigWidget, \
......@@ -1388,10 +1388,10 @@ class SpectralProfileRenderer(object):
pass
else:
style.setVisibility(False)
#symbol = renderer.sourceSymbol()
# symbol = renderer.sourceSymbol()
elif isinstance(symbol, (QgsMarkerSymbol, QgsLineSymbol, QgsFillSymbol)):
color: QColor = symbol.color()
color.setAlpha(int(symbol.opacity()*100))
color.setAlpha(int(symbol.opacity() * 100))
style.setLineColor(color)
style.setMarkerColor(color)
......@@ -2157,8 +2157,8 @@ class SpectralLibrary(QgsVectorLayer):
path: str = None,
baseName: str = DEFAULT_NAME,
options: QgsVectorLayer.LayerOptions = None,
uri: str = None, # deprectated
name: str = None # deprectated
uri: str = None, # deprectated
name: str = None # deprectated
):
if isinstance(uri, str):
......@@ -2623,10 +2623,10 @@ class SpectralLibrary(QgsVectorLayer):
msg = super(SpectralLibrary, self).exportNamedStyle(doc, context=context, categories=categories)
if msg == '':
qgsNode = doc.documentElement().toElement()
#speclibNode = doc.createElement(XMLNODE_PROFILE_RENDERER)
# speclibNode = doc.createElement(XMLNODE_PROFILE_RENDERER)
if isinstance(self.mProfileRenderer, SpectralProfileRenderer):
self.mProfileRenderer.writeXml(qgsNode, doc)
#qgsNode.appendChild(speclibNode)
# qgsNode.appendChild(speclibNode)
return msg
......@@ -2648,7 +2648,8 @@ class SpectralLibrary(QgsVectorLayer):
warnings.warn('Use SpectralLibrary.write() instead', DeprecationWarning)
return self.write(*args, **kwds)
def writeRasterImages(self, pathOne: typing.Union[str, pathlib.Path], drv:str='GTiff') -> typing.List[pathlib.Path]:
def writeRasterImages(self, pathOne: typing.Union[str, pathlib.Path], drv: str = 'GTiff') -> typing.List[
pathlib.Path]:
"""
Writes the SpectralLibrary into images of same spectral properties
:return: list of image paths
......@@ -2659,7 +2660,7 @@ class SpectralLibrary(QgsVectorLayer):
basename, ext = os.path.splitext(pathOne.name)
assert pathOne.parent.is_dir()
results = []
imageFiles = []
for k, profiles in self.groupBySpectralProperties().items():
xValues, xUnit, yUnit = k
ns: int = len(profiles)
......@@ -2667,26 +2668,29 @@ class SpectralLibrary(QgsVectorLayer):
ref_profile = np.asarray(profiles[0].yValues())
dtype = ref_profile.dtype
imageArray = np.empty((nb, ns, 1), dtype=dtype)
imageArray[:,0,0] = ref_profile
imageArray = np.empty((nb, 1, ns), dtype=dtype)
imageArray[:, 0, 0] = ref_profile
for i in range(1, len(profiles)):
imageArray[:, i, 0] = np.asarray(profiles[i].yValues(), dtype=dtype)
if len(results) == 0:
imageArray[:, 0, i] = np.asarray(profiles[i].yValues(), dtype=dtype)
if len(imageFiles) == 0:
pathDst = pathOne.parent / f'{basename}{ext}'
else:
pathDst = pathOne.parent / f'{basename}{i}{ext}'
pathDst = pathOne.parent / f'{basename}{len(imageFiles)}{ext}'
dsDst: gdal.Dataset = gdal_array.SaveArray(imageArray, pathDst.as_posix(), format=drv)
fakeProjection: osr.SpatialReference = osr.SpatialReference()
fakeProjection.SetFromUserInput('EPSG:3857')
dsDst.SetProjection(fakeProjection.ExportToWkt())
dsDst.SetGeoTransform([0.0, 1.0, 0.0, 0.0, 0.0, -1.0])
# north-up project, 1 px above equator, starting at 0°, n pixels = n profiles towards east
dsDst.SetGeoTransform([0.0, 1.0, 0.0, 1.0, 0.0, -1.0])
dsDst.SetMetadataItem('wavelength units', xUnit)
dsDst.SetMetadataItem('wavelength', ','.join(f'{v}' for v in xValues))
dsDst.FlushCache()
results.append(pathDst)
imageFiles.append(pathDst)
del dsDst
return results
return imageFiles
def write(self, path: str, **kwds) -> typing.List[str]:
"""
......@@ -2703,7 +2707,7 @@ class SpectralLibrary(QgsVectorLayer):
if path is None:
path, filter = QFileDialog.getSaveFileName(parent=kwds.get('parent'),
caption='Save Spectral Library',
directory='speclib',
directory=QgsFileUtils.stringToSafeFilename(self.name()),
filter=FILTERS)
if isinstance(path, pathlib.Path):
......@@ -2724,41 +2728,46 @@ class SpectralLibrary(QgsVectorLayer):
return []
def yRange(self):
profiles = self.profiles()
minY = min([min(p.yValues()) for p in profiles])
maxY = max([max(p.yValues()) for p in profiles])
def yRange(self) -> typing.List[float]:
"""
Returns the maximum y range
:return:
:rtype:
"""
minY = maxY = 0
for p in self.profiles():
yValues = p.yValues()
minY = min(minY, min(yValues))
maxY = max(maxY, max(yValues))
return minY, maxY
def __repr__(self):
return str(self.__class__) + '"{}" {} feature(s)'.format(self.name(), self.dataProvider().featureCount())
def plot(self):
def plot(self) -> QWidget:
"""Create a plot widget and shows all SpectralProfile in this SpectralLibrary."""
from ..externals import pyqtgraph as pg
pg.mkQApp()
win = pg.GraphicsWindow(title="Spectral Library")
win.resize(1000, 600)
app = None
if not isinstance(QgsApplication.instance(), QgsApplication):
from ..testing import start_app
app = start_app()
# Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True)
from .gui import SpectralLibraryWidget
# Create a plot with some random data
p1 = win.addPlot(title="Spectral Library {}".format(self.name()), pen=0.5)
yMin, yMax = self.yRange()
p1.setYRange(yMin, yMax)
w = SpectralLibraryWidget(speclib=self)
w.show()
# Add three infinite lines with labels
for p in self:
pi = pg.PlotDataItem(p.xValues(), p.yValues())
p1.addItem(pi)
if app:
app.exec_()
pg.QAPP.exec_()
return w
def fieldNames(self) -> list:
"""
Retunrs the field names. Shortcut from self.fields().names()
Returns the field names. Shortcut from self.fields().names()
:return: [list-of-str]
"""
return self.fields().names()
......
......@@ -42,7 +42,7 @@ from qgis.core import \
QgsFeature, QgsRenderContext, QgsNullSymbolRenderer, \
QgsRasterLayer, QgsMapLayer, QgsVectorLayer, \
QgsSymbol, QgsMarkerSymbol, QgsLineSymbol, QgsFillSymbol, \
QgsAttributeTableConfig, QgsField, QgsMapLayerProxyModel
QgsAttributeTableConfig, QgsField, QgsMapLayerProxyModel, QgsFileUtils
from qgis.gui import \
QgsEditorWidgetWrapper, QgsAttributeTableView, \
QgsActionMenu, QgsEditorWidgetFactory, QgsStatusBar, \
......@@ -808,8 +808,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
self.mXAxis: SpectralXAxis = pi.getAxis('bottom')
assert isinstance(self.mXAxis, SpectralXAxis)
self.mSpeclib: SpectralLibrary
self.mSpeclib = None
self.mSpeclib: SpectralLibrary = None
self.mSpeclibSignalConnections = []
self.mXUnitInitialized = False
......@@ -2155,7 +2154,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
from .io.specchio import SPECCHIOSpectralLibraryIO
from .io.artmo import ARTMOSpectralLibraryIO
from .io.vectorsources import VectorSourceSpectralLibraryIO
from .io.rastersources import RasterSourceSpectralLibraryIO
self.mSpeclibIOInterfaces = [
EnviSpectralLibraryIO(),
CSVSpectralLibraryIO(),
......@@ -2164,6 +2163,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
EcoSISSpectralLibraryIO(),
SPECCHIOSpectralLibraryIO(),
VectorSourceSpectralLibraryIO(),
RasterSourceSpectralLibraryIO(),
]
self.mSpeclibIOInterfaces = sorted(self.mSpeclibIOInterfaces, key=lambda c: c.__class__.__name__)
......@@ -2233,6 +2233,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
self.actionExportSpeclib = QAction('Export Spectral Profiles')
self.actionExportSpeclib.setToolTip('Export spectral profiles to other data formats')
self.actionExportSpeclib.setIcon(QIcon(':/qps/ui/icons/speclib_save.svg'))
m = QMenu()
self.createSpeclibExportMenu(m)
self.actionExportSpeclib.setMenu(m)
......@@ -2337,17 +2338,39 @@ class SpectralLibraryWidget(AttributeTableWidget):
"""
:return: QMenu with QActions and submenus to import SpectralProfiles
"""
separated = []
from .io.rastersources import RasterSourceSpectralLibraryIO
for iface in self.mSpeclibIOInterfaces:
assert isinstance(iface, AbstractSpectralLibraryIO), iface
iface.addImportActions(self.speclib(), menu)
if isinstance(iface, RasterSourceSpectralLibraryIO):
separated.append(iface)
else:
iface.addImportActions(self.speclib(), menu)
if len(separated) > 0:
menu.addSeparator()
for iface in separated:
iface.addImportActions(self.speclib(), menu)
def createSpeclibExportMenu(self, menu: QMenu):
"""
:return: QMenu with QActions and submenus to export SpectralProfiles
:return: QMenu with QActions and submenus to export the SpectralLibrary
"""
separated = []
from .io.rastersources import RasterSourceSpectralLibraryIO
for iface in self.mSpeclibIOInterfaces:
assert isinstance(iface, AbstractSpectralLibraryIO)
iface.addExportActions(self.speclib(), menu)
if isinstance(iface, RasterSourceSpectralLibraryIO):
separated.append(iface)
else:
iface.addExportActions(self.speclib(), menu)
if len(separated) > 0:
menu.addSeparator()
for iface in separated:
iface.addExportActions(self.speclib(), menu)
def plotWidget(self) -> SpectralLibraryPlotWidget:
return self.mPlotWidget
......@@ -2529,7 +2552,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
self.speclib().startEditing()
def onExportSpectra(self, *args):
files = self.mSpeclib.write(None)
files = self.speclib().write(None)
if len(files) > 0:
self.sigFilesCreated.emit(files)
......
......@@ -24,19 +24,21 @@
along with this software. If not, see <http://www.gnu.org/licenses/>.
***************************************************************************
"""
import os
import sys
import typing
from osgeo import gdal
import numpy as np
from qgis.PyQt import sip
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets import *
from qgis.PyQt.QtCore import *
from qgis.core import QgsTask, QgsMapLayer, QgsVectorLayer, QgsRasterLayer, QgsWkbTypes, \
QgsTaskManager, QgsMapLayerProxyModel, QgsApplication
from ..core import SpectralProfile, SpectralLibrary
from ...utils import SelectMapLayersDialog
QgsTaskManager, QgsMapLayerProxyModel, QgsApplication, QgsFileUtils
from ..core import SpectralProfile, SpectralLibrary, AbstractSpectralLibraryIO, ProgressHandler
from ...utils import SelectMapLayersDialog, gdalDataset, parseWavelength, parseFWHM, parseBadBandList
PIXEL_LIMIT = 100*100
class SpectralProfileLoadingTask(QgsTask):
......@@ -240,3 +242,133 @@ class SpectralProfileImportPointsDialog(SelectMapLayersDialog):
:return: QgsVectorLayer
"""
return self.mapLayers()[1]
class RasterSourceSpectralLibraryIO(AbstractSpectralLibraryIO):
"""
I/O Interface for Raster files.
"""
@classmethod
def canRead(cls, path: str) -> bool:
"""
Returns true if it can read the source defined by path
:param path: source uri
:return: True, if source is readable.
"""
path = str(path)
try:
ds = gdalDataset(path)
return True
except:
return False
return False
@classmethod
def readFrom(cls, path,
progressDialog: typing.Union[QProgressDialog, ProgressHandler] = None,
addAttributes: bool = True) -> SpectralLibrary:
ds: gdal.Dataset = gdalDataset(path)
if not isinstance(ds, gdal.Dataset):
return None
speclib = SpectralLibrary()
assert isinstance(speclib, SpectralLibrary)
sourcepath = ds.GetDescription()
basename = os.path.basename(ds.GetDescription())
speclib.setName(basename)
assert speclib.startEditing()
wl, wlu = parseWavelength(ds)
fwhm = parseFWHM(ds)
bbl = parseBadBandList(ds)
# each none-masked pixel is a profile
array = ds.ReadAsArray()
ref_band: gdal.Band = ds.GetRasterBand(1)
no_data = ref_band.GetNoDataValue()
valid = np.isfinite(array[0, :])
if no_data:
valid = valid * (array[0, :] != no_data)
valid = np.where(valid)
n_profiles = len(valid[0])
if n_profiles > PIXEL_LIMIT:
raise Exception(f'Number of raster image pixels {n_profiles} exceeds PIXEL_LIMIT {PIXEL_LIMIT}')
if wl is not None:
xvalues = wl.tolist()
else:
xvalues = (np.arange(ds.RasterCount) + 1).tolist()
profiles = []
for y, x in zip(*valid):
yvalues = array[:, y, x]
p = SpectralProfile(fields=speclib.fields())
p.setName(f'Profile {x},{y}')
p.setSource(basename)
p.setValues(xvalues, yvalues, xUnit=wlu)
profiles.append(p)
speclib.addProfiles(profiles)
speclib.commitChanges()
return speclib
@classmethod
def write(cls, speclib: SpectralLibrary,
path: str,
progressDialog: typing.Union[QProgressDialog, ProgressHandler] = None):
"""
Writes the SpectralLibrary to path and returns a list of written files that can be used to open the spectral library with readFrom
"""
speclib.writeRasterImages(path)
return [path]
@classmethod
def addImportActions(cls, spectralLibrary: SpectralLibrary, menu: QMenu) -> list:
def read(speclib: SpectralLibrary):
path, filter = QFileDialog.getOpenFileName(caption='Raster Image',
filter='All types (*.*)')
if os.path.isfile(path):
if not RasterSourceSpectralLibraryIO.canRead(path):
QMessageBox.critical(None, 'Raster image as SpectralLibrary', f'Unable to reads {path}')
try:
sl = RasterSourceSpectralLibraryIO.readFrom(path)
if isinstance(sl, SpectralLibrary):
speclib.startEditing()
speclib.beginEditCommand('Add Spectral Library from {}'.format(path))
speclib.addSpeclib(sl, addMissingFields=True)
speclib.endEditCommand()
speclib.commitChanges()
except Exception as ex:
QMessageBox.critical(None, 'Raster image as SpectralLibrary', str(ex))
return
m = menu.addAction('Raster Image')
m.setToolTip('Import all pixels as spectral profiles which are not masked. '
'Use careful and not with large images!')
m.triggered.connect(lambda *args, sl=spectralLibrary: read(sl))
@classmethod
def addExportActions(cls, spectralLibrary: SpectralLibrary, menu: QMenu) -> list:
def write(speclib: SpectralLibrary):
# https://gdal.org/drivers/vector/index.html
LUT_Files = {'GeoTiff (*.tif)': 'GTiff',
'ENVI Raster (*.bsq)': 'ENVI',
}
path, filter = QFileDialog.getSaveFileName(caption='Write as raster image',
filter=';;'.join(LUT_Files.keys()),
directory=QgsFileUtils.stringToSafeFilename(speclib.name()))
if isinstance(path, str) and len(path) > 0:
speclib.writeRasterImages(path, drv=LUT_Files.get(filter, 'GTiff'))
a = menu.addAction('Raster Image')
a.setToolTip('Write profiles as raster image(s), grouped by wavelengths.')
a.triggered.connect(lambda *args, sl=spectralLibrary: write(sl))
......@@ -34,9 +34,9 @@ import typing
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtWidgets import *
from qgis.core import *
from qgis.gui import *
from qgis.core import QgsField, QgsVectorLayer, QgsVectorFileWriter, QgsProviderRegistry, QgsProject
from qgis.core import QgsField, QgsVectorLayer, QgsVectorFileWriter, QgsProviderRegistry, \
QgsProject, QgsProviderMetadata, QgsFileUtils
from ..core import SpectralProfile, SpectralLibrary, AbstractSpectralLibraryIO, \
decodeProfileValueDict, encodeProfileValueDict, \
......@@ -82,8 +82,7 @@ class VectorSourceFieldValueConverter(QgsVectorFileWriter.FieldValueConverter):
class VectorSourceSpectralLibraryIO(AbstractSpectralLibraryIO):
"""
I/O Interface for the EcoSIS spectral library format.
See https://ecosis.org for details.
I/O Interface for Vector File Formats.
"""
@classmethod
......@@ -118,6 +117,8 @@ class VectorSourceSpectralLibraryIO(AbstractSpectralLibraryIO):
addAttributes: bool = True) -> SpectralLibrary:
"""
Returns the SpectralLibrary read from "path"
:param progressDialog:
:type progressDialog:
:param path: source of SpectralLibrary
:return: SpectralLibrary
"""
......@@ -252,7 +253,9 @@ class VectorSourceSpectralLibraryIO(AbstractSpectralLibraryIO):
def read(speclib: SpectralLibrary):
path, filter = QFileDialog.getOpenFileName(caption='Vector File',
filter='All type (*.*)')
filter='All type (*.*)',
directory=QgsFileUtils.stringToSafeFilename(speclib.name())
)
if os.path.isfile(path) and VectorSourceSpectralLibraryIO.canRead(path):
sl = VectorSourceSpectralLibraryIO.readFrom(path)
if isinstance(sl, SpectralLibrary):
......@@ -269,7 +272,7 @@ class VectorSourceSpectralLibraryIO(AbstractSpectralLibraryIO):
@classmethod
def addExportActions(cls, spectralLibrary: SpectralLibrary, menu: QMenu) -> list:
"""
def write_new(speclib: SpectralLibrary):
# this is not available in Python. Why?
from qgis.gui import QgsVectorLayerSaveAsDialog
......@@ -282,7 +285,7 @@ class VectorSourceSpectralLibraryIO(AbstractSpectralLibraryIO):
d = QgsVectorLayerSaveAsDialog(speclib, options=options)
d.show()
s = ""
"""
def write(speclib: SpectralLibrary):
# https://gdal.org/drivers/vector/index.html
......
......@@ -129,7 +129,8 @@ def start_app(cleanup=True, options=StartOptions.Minimized, resources: list = []
qgsApp.addLibraryPath(candidate.as_posix())
assert QgsProviderRegistry.instance().libraryDirectory().exists(), \
'Directory: {} does not exist'.format(QgsProviderRegistry.instance().libraryDirectory().path())
'Directory: {} does not exist. Please check if QGIS_PREFIX_PATH correct'.format(
QgsProviderRegistry.instance().libraryDirectory().path())
# initiate a PythonRunner instance if None exists
if StartOptions.PythonRunner in options and not QgsPythonRunner.isValid():
......@@ -518,9 +519,16 @@ class TestObjects():
coredata, wl, wlu, gt, wkt = TestObjects.coreData()
cnb, cnl, cns = coredata.shape
assert n > 0
if not isinstance(n_bands, list):
n_bands = [n_bands]
assert isinstance(n_bands, list)
for nb in n_bands:
assert nb == -1 or nb > 0 and nb <= cnb
for i in range(len(n_bands)):
nb = n_bands[i]
if nb == -1:
n_bands[i] = cnb
else:
assert 0 < nb <= cnb, f'Number of bands need to be in range 0 < nb <= {cnb}.'
n_bands = [nb if nb > 0 else cnb for nb in n_bands]
for nb in n_bands:
......@@ -544,7 +552,7 @@ class TestObjects():
for (data, wl, data_wlu) in TestObjects.spectralProfileData(n, n_bands=n_bands):
if wlu is None:
wlu = data_wlu
if wlu != data_wlu:
elif wlu != data_wlu:
wl = UnitLookup.convertMetricUnit(wl, data_wlu, wlu)
profile = SpectralProfile(fields=fields)
......@@ -564,6 +572,8 @@ class TestObjects():
wlu: str = None):
"""
Creates an Spectral Library
:param n_bands:
:type n_bands:
:param wlu:
:type wlu:
:param n: total number of profiles
......@@ -717,7 +727,9 @@ class TestObjects():
wl = core_wl[:nb].tolist()
assert len(wl) == nb
if wlu != core_wlu:
if wlu is None:
wlu = core_wlu
elif wlu != core_wlu:
wl = UnitLookup.convertMetricUnit(wl, core_wlu, wlu)
domain = None
......
......@@ -19,20 +19,18 @@
# noinspection PyPep8Naming
import os
import sys
import unittest
import xmlrunner
from qgis.PyQt.QtWidgets import QApplication
from qgis.core import QgsRasterLayer
from eotimeseriesviewer.tests import start_app, EOTSVTestCase
from eotimeseriesviewer.utils import nextColor
from osgeo import gdal_array
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtCore import *
import unittest, tempfile
from osgeo import gdal_array, osr
from eotimeseriesviewer.stackedbandinput import *
from example.Images import Img_2014_06_16_LE72270652014167CUB00_BOA, Img_2014_05_07_LC82270652014127LGN00_BOA
from eotimeseriesviewer.main import EOTimeSeriesViewer
class TestStackedInputs(EOTSVTestCase):
def setUp(self):
......@@ -42,7 +40,6 @@ class TestStackedInputs(EOTSVTestCase):
eotsv.close()
QApplication.processEvents()
def createTestDatasets(self):
vsiDir = r'/vsimem/tmp'
......@@ -60,7 +57,7 @@ class TestStackedInputs(EOTSVTestCase):