Skip to content

Commit 904c482

Browse files
7.10.1
1 parent d8a7c11 commit 904c482

27 files changed

Lines changed: 25489 additions & 25280 deletions

__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def description():
4242

4343

4444
def version():
45-
return 'Version 7.10.0 - Matera'
45+
return 'Version 7.10.1 - Matera'
4646

4747

4848
def icon():

core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,7 @@
799799
functionNames.append([[' remove_bandset', 'cfg.batchT.performRemoveBandSet', 'cfg.bst.removeBandSetTab', ['band_set : 1', 'unload_bands : 0']]])
800800
functionNames.append([[' select_bandset', 'cfg.batchT.performBandSetSelection', 'cfg.bst.selectBandSetTab', ['band_set : 1']]])
801801
functionNames.append([['Band calculation']])
802-
functionNames.append([[' band_calc', 'cfg.batchT.performBandCalc', 'cfg.bCalc.calculate', ['expression : \'\'', 'output_raster_path : \'\'', 'extent_same_as_raster_name : \'\'', 'align : 1', 'extent_intersection : 1', 'input_nodata_as_value : 0', 'use_value_nodata : 0', 'calculation_data_type : \'Float32\'', 'output_nodata_value : -32768', 'data_type : \'Float32\'', 'scale_value : 1', 'offset_value : 0', 'band_set : 1']]])
802+
functionNames.append([[' band_calc', 'cfg.batchT.performBandCalc', 'cfg.bCalc.calculate', ['expression : \'\'', 'output_raster_path : \'\'', 'extent_same_as_raster_name : \'\'', 'align : 1', 'extent_intersection : 1', 'input_nodata_as_value : 0', 'use_value_nodata : 0', 'calculation_data_type : \'Float32\'', 'output_nodata_value : -32768', 'data_type : \'Float32\'', 'nodata_mask : 1', 'scale_value : 1', 'offset_value : 0', 'band_set : 1']]])
803803
functionNames.append([['Preprocessing']])
804804
functionNames.append([[' aster_conversion', 'cfg.batchT.performASTERConversion', 'cfg.ASTERT.ASTER', ['input_raster_path : \'\'', 'celsius_temperature : 0', 'apply_dos1 : 0', 'use_nodata : 1', 'nodata_value : 0', 'create_bandset : 1', 'output_dir : \'\'', 'band_set : 1']]])
805805
functionNames.append([[' clip_multiple_rasters', 'cfg.batchT.performClipRasters', 'cfg.clipMulti.clipRasters', ['band_set : 1', 'output_dir : \'\'', 'use_vector : 0', 'vector_path : \'\'', 'use_vector_field : 0', 'vector_field : \'\'', 'ul_x : \'\'', 'ul_y : \'\'', 'lr_x : \'\'', 'lr_y : \'\'', 'nodata_value : 0', 'output_name_prefix : \'clip\'']]])

core/utils.py

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ def createSignatureClassRaster(self, signatureList, gdalRasterRef, outputDirecto
911911
return oRL, outputRasterList
912912

913913
# perform classification
914-
def classificationMultiprocess(self, bandListNumber, signatureList, algorithmName, rasterArray, landCoverSignature, LCSClassAlgorithm,LCSLeaveUnclassified, algBandWeigths, outputGdalRasterList, outputAlgorithmRaster, outputClassificationRaster, nodataValue, macroclassCheck, algThrshld):
914+
def classificationMultiprocess(self, bandListNumber, signatureList, algorithmName, rasterArray, landCoverSignature, LCSClassAlgorithm,LCSLeaveUnclassified, algBandWeigths, outputGdalRasterList, outputAlgorithmRaster, outputClassificationRaster, nodataValue, macroclassCheck, algThrshld, nodataMask):
915915
sigArrayList = self.createArrayFromSignature(bandListNumber, signatureList)
916916
# logger
917917
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'sigArrayList ' + str(sigArrayList))
@@ -1096,6 +1096,8 @@ def classificationMultiprocess(self, bandListNumber, signatureList, algorithmNam
10961096
else:
10971097
return 'No'
10981098
classArrayWrite = cfg.np.where(classArrayLCS == nodataValue, 0, classArrayLCS)
1099+
if nodataMask is not None:
1100+
classArray[::, ::][nodataMask[::, ::] != 0] = nodataMask[::, ::][nodataMask[::, ::] != 0]
10991101
# classification raster
11001102
cfg.utls.writeArrayBlock(rDC, 1, classArrayWrite, 0, 0, nodataValue)
11011103
n = n + 1
@@ -1145,6 +1147,8 @@ def classificationMultiprocess(self, bandListNumber, signatureList, algorithmNam
11451147
e = None
11461148
clA = None
11471149
classArray[classArray == cfg.unclassifiedVal] = 0
1150+
if nodataMask is not None:
1151+
classArray[::, ::][nodataMask[::, ::] != 0] = nodataMask[::, ::][nodataMask[::, ::] != 0]
11481152
# classification raster
11491153
self.writeArrayBlock(rDC, 1, classArray, 0, 0, nodataValue)
11501154
n = n + 1
@@ -1187,6 +1191,8 @@ def classificationMultiprocess(self, bandListNumber, signatureList, algorithmNam
11871191
e = None
11881192
clA = None
11891193
classArray[classArray == cfg.unclassifiedVal] = 0
1194+
if nodataMask is not None:
1195+
classArray[::, ::][nodataMask[::, ::] != 0] = nodataMask[::, ::][nodataMask[::, ::] != 0]
11901196
# classification raster
11911197
self.writeArrayBlock(rDC, 1, classArray, 0, 0, nodataValue)
11921198
n = n + 1
@@ -1227,6 +1233,8 @@ def classificationMultiprocess(self, bandListNumber, signatureList, algorithmNam
12271233
e = None
12281234
clA = None
12291235
classArray[classArray == cfg.unclassifiedVal] = 0
1236+
if nodataMask is not None:
1237+
classArray[::, ::][nodataMask[::, ::] != 0] = nodataMask[::, ::][nodataMask[::, ::] != 0]
12301238
# classification raster
12311239
self.writeArrayBlock(rDC, 1, classArray, 0, 0, nodataValue)
12321240
n = n + 1
@@ -2672,16 +2680,21 @@ def createVirtualRaster2(self, inputRasterList, output, bandNumberList = None, N
26722680
bottoms.append(bottom)
26732681
pXSizes.append(pX)
26742682
pYSizes.append(pY)
2675-
if intersection == 'No':
2676-
iLeft = min(lefts)
2677-
iTop = max(tops)
2678-
iRight = max(rights)
2679-
iBottom = min(bottoms)
2680-
else:
2681-
iLeft = max(lefts)
2682-
iTop = min(tops)
2683-
iRight = min(rights)
2684-
iBottom = max(bottoms)
2683+
try:
2684+
if intersection == 'No':
2685+
iLeft = min(lefts)
2686+
iTop = max(tops)
2687+
iRight = max(rights)
2688+
iBottom = min(bottoms)
2689+
else:
2690+
iLeft = max(lefts)
2691+
iTop = min(tops)
2692+
iRight = min(rights)
2693+
iBottom = max(bottoms)
2694+
except Exception as err:
2695+
# logger
2696+
cfg.utls.logCondition(str(__name__) + '-' + (cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), ' ERROR exception: ' + str(err))
2697+
return None
26852698
pXSize = min(pXSizes)
26862699
pYSize = min(pYSizes)
26872700
# create virtual raster
@@ -3086,10 +3099,12 @@ def readArrayBlock(self, gdalBand, pixelStartColumn, pixelStartRow, blockColumns
30863099
except:
30873100
o = 0.0
30883101
s = 1.0
3102+
o = cfg.np.asarray(o).astype(calcDataType)
3103+
s = cfg.np.asarray(s).astype(calcDataType)
30893104
# logger
30903105
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 's ' + str(s)+ ' o ' + str(o))
30913106
try:
3092-
a = gdalBand.ReadAsArray(pixelStartColumn, pixelStartRow, blockColumns, blockRow) * cfg.np.asarray(s).astype(calcDataType) + cfg.np.asarray(o).astype(calcDataType)
3107+
a = cfg.np.asarray(gdalBand.ReadAsArray(pixelStartColumn, pixelStartRow, blockColumns, blockRow) * s + o).astype(calcDataType)
30933108
# logger
30943109
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'a ' + str(a[0,0]))
30953110
except:
@@ -3109,7 +3124,11 @@ def writeArrayBlock(self, gdalRaster, bandNumber, dataArray, pixelStartColumn, p
31093124
dataArray = dataArray[:y, :x]
31103125
b.WriteArray(dataArray, pixelStartColumn, pixelStartRow)
31113126
if nodataValue is not None:
3112-
b.SetNoDataValue(nodataValue)
3127+
try:
3128+
b.SetNoDataValue(nodataValue)
3129+
except Exception as err:
3130+
# logger
3131+
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'Error ' + str(err))
31133132
b.FlushCache()
31143133
b = None
31153134

@@ -4736,6 +4755,7 @@ def processRasterDev(self, raster = None, signatureList = None, functionBand = N
47364755
vBY = writerLog[13]
47374756
calcDataType = writerLog[14]
47384757
GDALDLLPath = writerLog[15]
4758+
useNoDataMask = writerLog[16]
47394759
for d in GDALDLLPath.split(';'):
47404760
try:
47414761
os.add_dll_directory(d)
@@ -4951,10 +4971,10 @@ def processRasterDev(self, raster = None, signatureList = None, functionBand = N
49514971
sclB = sclB
49524972
if offsB is not None:
49534973
offsB = offsB
4954-
ndvBand = b0.GetNoDataValue()
4955-
# logger
4956-
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'ndvBand ' + str(ndvBand) )
4957-
ndvBand = ndvBand * sclB + offsB
4974+
offsB = cfg.np.asarray(offsB).astype(a.dtype)
4975+
sclB = cfg.np.asarray(sclB).astype(a.dtype)
4976+
ndvBand = b0.GetNoDataValue()
4977+
ndvBand = cfg.np.asarray(ndvBand * sclB + offsB).astype(a.dtype)
49584978
except:
49594979
try:
49604980
offsB = gdalBandList[b].GetOffset()
@@ -4967,15 +4987,17 @@ def processRasterDev(self, raster = None, signatureList = None, functionBand = N
49674987
offsB = offsB
49684988
else:
49694989
offsB = 0
4990+
offsB = cfg.np.asarray(offsB).astype(a.dtype)
4991+
sclB = cfg.np.asarray(sclB).astype(a.dtype)
49704992
ndvBand = gdalBandList[b].GetNoDataValue()
4971-
ndvBand = ndvBand * sclB + offsB
4993+
ndvBand = cfg.np.asarray(ndvBand * sclB + offsB).astype(a.dtype)
49724994
except:
49734995
ndvBand = None
49744996
# logger
49754997
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'ndvBand ' + str(ndvBand))
49764998
if ndvBand is not None:
49774999
# adapt NoData to dtype
4978-
ndvBand = cfg.np.asarray(ndvBand).astype(a.dtype).astype(calcDataType)
5000+
ndvBand = cfg.np.asarray(ndvBand).astype(calcDataType)
49795001
# logger
49805002
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'ndvBand ' + str(ndvBand) + ' sclB ' + str(sclB)+ ' offsB ' + str(offsB))
49815003
if a is not None:
@@ -4986,7 +5008,6 @@ def processRasterDev(self, raster = None, signatureList = None, functionBand = N
49865008
# logger
49875009
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'a[0, 0] ' + str(a[0, 0] ) )
49885010
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'array[0, 0, b] ' + str(array[0, 0, b] ) )
4989-
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'ndvBand ' + str(ndvBand.astype(calcDataType)) )
49905011
else:
49915012
procError = 'Error array none'
49925013
# set nodata value
@@ -5001,34 +5022,35 @@ def processRasterDev(self, raster = None, signatureList = None, functionBand = N
50015022
array[::, ::, b][cfg.np.isnan(array[::, ::, b])] = ndvBand
50025023
except:
50035024
pass
5004-
try:
5005-
nodataMask[0, 0]
5006-
except:
5007-
nodataMask = cfg.np.zeros((bSY, bSX), dtype=calcDataType)
5008-
if skipReplaceNoData is not None:
5009-
pass
5010-
elif ndvBand is not None:
5025+
if useNoDataMask == 'Yes':
50115026
try:
5012-
nodataMask[::, ::][array[::, ::, b] == ndvBand] = outputNoData
5027+
nodataMask[0, 0]
50135028
except:
5029+
nodataMask = cfg.np.zeros((bSY, bSX), dtype=calcDataType)
5030+
if skipReplaceNoData is not None:
50145031
pass
5015-
if skipReplaceNoData is not None:
5016-
pass
5017-
else:
5018-
try:
5019-
nodataMask[::, ::][cfg.np.isnan(array[::, ::, b])] = outputNoData
5020-
except:
5032+
elif ndvBand is not None:
5033+
try:
5034+
nodataMask[::, ::][array[::, ::, b] == ndvBand] = outputNoData
5035+
except:
5036+
pass
5037+
if skipReplaceNoData is not None:
50215038
pass
5022-
if skipReplaceNoData is not None:
5023-
pass
5024-
elif nodataValue is not None:
5025-
nodataValue = cfg.np.asarray(nodataValue).astype(a.dtype).astype(calcDataType)
5026-
# logger
5027-
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'nodataValue ' + str(nodataValue))
5028-
try:
5029-
nodataMask[::, ::][array[::, ::, b] == nodataValue] = outputNoData
5030-
except:
5039+
else:
5040+
try:
5041+
nodataMask[::, ::][cfg.np.isnan(array[::, ::, b])] = outputNoData
5042+
except:
5043+
pass
5044+
if skipReplaceNoData is not None:
50315045
pass
5046+
elif nodataValue is not None:
5047+
nodataValue = cfg.np.asarray(nodataValue).astype(a.dtype).astype(calcDataType)
5048+
# logger
5049+
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'nodataValue ' + str(nodataValue))
5050+
try:
5051+
nodataMask[::, ::][array[::, ::, b] == nodataValue] = outputNoData
5052+
except:
5053+
pass
50325054
# logger
50335055
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'outputNoData ' + str(outputNoData))
50345056
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'a[0,0] ' + str(a[0, 0]))
@@ -5072,7 +5094,7 @@ def processRasterDev(self, raster = None, signatureList = None, functionBand = N
50725094
# logger
50735095
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'outputSigRasterList ' + str(outputSigRasterList))
50745096
landCoverSignature, LCSClassAlgorithm, LCSLeaveUnclassified, algBandWeigths, algThrshld = classificationOptions
5075-
oo = functionRaster(len(gdalBandList), signatureList, algorithmName, array, landCoverSignature, LCSClassAlgorithm,LCSLeaveUnclassified, algBandWeigths, outputSigRasterList, outAlg, outClass, nodataValue, macroclassCheck, algThrshld)
5097+
oo = functionRaster(len(gdalBandList), signatureList, algorithmName, array, landCoverSignature, LCSClassAlgorithm,LCSLeaveUnclassified, algBandWeigths, outputSigRasterList, outAlg, outClass, outputNoData, macroclassCheck, algThrshld, nodataMask)
50765098
# logger
50775099
cfg.utls.logToFile(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'oo ' + str(oo))
50785100
o = [outClasses, outAlgs, outSigDict]
@@ -5162,14 +5184,17 @@ def processRasterDev(self, raster = None, signatureList = None, functionBand = N
51625184
return o, ''
51635185

51645186
# process a raster with block size
5165-
def multiProcessRaster(self, rasterPath, signatureList = None, functionBand = None, functionRaster = None, algorithmName = None, outputRasterList = None, outputAlgorithmRaster = None, outputClassificationRaster = None, classificationOptions = None, nodataValue = None, macroclassCheck = 'No', functionBandArgument = None, functionVariable = None, progressMessage = "", skipReplaceNoData = None, threadNumber = None, parallel = None, deleteArray = None, skipSingleBand = None, outputBandNumber = None, virtualRaster = 'No', compress = 'No', compressFormat = 'LZW', outputNoDataValue = None, dataType = None, scale = None, offset = None, parallelWritingCheck = None, boundarySize = None, additionalLayer = None, calcDataType = None):
5187+
def multiProcessRaster(self, rasterPath, signatureList = None, functionBand = None, functionRaster = None, algorithmName = None, outputRasterList = None, outputAlgorithmRaster = None, outputClassificationRaster = None, classificationOptions = None, nodataValue = None, macroclassCheck = 'No', functionBandArgument = None, functionVariable = None, progressMessage = "", skipReplaceNoData = None, threadNumber = None, parallel = None, deleteArray = None, skipSingleBand = None, outputBandNumber = None, virtualRaster = 'No', compress = 'No', compressFormat = 'LZW', outputNoDataValue = None, dataType = None, scale = None, offset = None, parallelWritingCheck = None, boundarySize = None, additionalLayer = None, calcDataType = None, nodataMask = None):
51665188
# logger
51675189
cfg.utls.logCondition(str(__name__) + '-' + str(cfg.inspectSCP.stack()[0][3])+ ' ' + cfg.utls.lineOfCode(), 'start processRaster')
51685190
if outputNoDataValue is None:
51695191
outputNoDataValue = cfg.NoDataVal
51705192
if dataType is None:
51715193
dataType = cfg.rasterDataType
51725194
unitMemory = cfg.arrayUnitMemory
5195+
# nodata mask
5196+
if nodataMask is None:
5197+
nodataMask = 'Yes'
51735198
# calculation data type
51745199
if calcDataType is None:
51755200
calcDataType = cfg.np.float32
@@ -5328,7 +5353,7 @@ def multiProcessRaster(self, rasterPath, signatureList = None, functionBand = No
53285353
roY = min(vY)
53295354
if cfg.actionCheck == 'Yes':
53305355
pOut = ''
5331-
wrtP = [p, outputRasterList, cfg.tmpDir, parallelWritingCheck, pMQ, memVal, compress, compressFormat, dataType, boundarySize, x, roY, bSX, vBY, calcDataType, cfg.gdalDLLPath]
5356+
wrtP = [p, outputRasterList, cfg.tmpDir, parallelWritingCheck, pMQ, memVal, compress, compressFormat, dataType, boundarySize, x, roY, bSX, vBY, calcDataType, cfg.gdalDLLPath, nodataMask]
53325357
c = cfg.pool.apply_async(self.processRasterDev, args=(rasterPath, signatureList, functionBand, functionRaster, algorithmName, pOut, outputAlgorithmRaster, outputClassificationRaster, sections, classificationOptions, nodataValue, macroclassCheck, functionBandArgument, functionVariable, progressMessage, skipReplaceNoData, singleBandNumber, outputBandNumber, outputNoDataValue, scale, offset, wrtP))
53335358
results.append([c, p])
53345359
cfg.QtWidgetsSCP.qApp.processEvents()
@@ -5418,7 +5443,7 @@ def multiProcessRaster(self, rasterPath, signatureList = None, functionBand = No
54185443
fArg = functionBandArgument[p]
54195444
fVar = functionVariable[p]
54205445
otpLst = None
5421-
wrtP = [p, otpLst, cfg.tmpDir, parallelWritingCheck, pMQ, memVal, compress, compressFormat, dataType, boundarySize, 0, 0, rX, rY, calcDataType, cfg.gdalDLLPath]
5446+
wrtP = [p, otpLst, cfg.tmpDir, parallelWritingCheck, pMQ, memVal, compress, compressFormat, dataType, boundarySize, 0, 0, rX, rY, calcDataType, cfg.gdalDLLPath, nodataMask]
54225447
if skipSingleBand is None:
54235448
singleBand = p
54245449
else:
@@ -6467,10 +6492,13 @@ def questionBox(self, caption, message):
64676492

64686493
# show hide input image
64696494
def showHideInputImage(self):
6470-
if cfg.bandSetsList[cfg.bndSetNumber][0] == 'Yes':
6471-
i = cfg.tmpVrtDict[cfg.bndSetNumber]
6472-
else:
6473-
i = cfg.utls.selectLayerbyName(cfg.bandSetsList[cfg.bndSetNumber][8], 'Yes')
6495+
try:
6496+
if cfg.bandSetsList[cfg.bndSetNumber][0] == 'Yes':
6497+
i = cfg.tmpVrtDict[cfg.bndSetNumber]
6498+
else:
6499+
i = cfg.utls.selectLayerbyName(cfg.bandSetsList[cfg.bndSetNumber][8], 'Yes')
6500+
except:
6501+
pass
64746502
try:
64756503
if i is not None:
64766504
if cfg.inputImageRadio.isChecked():

docs/repository.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?xml version = '1.0' encoding = 'UTF-8'?>
22
<plugins>
3-
<pyqgis_plugin name="Semi-Automatic Classification Plugin - master" version="7.10.0" plugin_id="284">
3+
<pyqgis_plugin name="Semi-Automatic Classification Plugin - master" version="7.10.1" plugin_id="284">
44
<description><![CDATA[The Semi-Automatic Classification Plugin (SCP) allows for the supervised classification of remote sensing images, providing tools for the download, the preprocessing and postprocessing of images.]]></description>
55
<about><![CDATA[Developed by Luca Congedo, the Semi-Automatic Classification Plugin (SCP) allows for the supervised classification of remote sensing images, providing tools for the download, the preprocessing and postprocessing of images. Search and download is available for ASTER, GOES, Landsat, MODIS, Sentinel-1, Sentinel-2, and Sentinel-3 images. Several algorithms are available for the land cover classification. This plugin requires the installation of GDAL, OGR, Numpy, SciPy, and Matplotlib. Some tools require also the installation of SNAP (ESA Sentinel Application Platform). For more information please visit https://fromgistors.blogspot.com .]]></about>
6-
<version>7.10.0</version>
6+
<version>7.10.1</version>
77
<qgis_minimum_version>3.0.0</qgis_minimum_version>
88
<qgis_maximum_version>3.99.0</qgis_maximum_version>
99
<homepage><![CDATA[https://fromgistors.blogspot.com/p/semi-automatic-classification-plugin.html]]></homepage>

0 commit comments

Comments
 (0)