Skip to content

Commit

Permalink
Merge pull request #2 from fire2a/separate-minmax
Browse files Browse the repository at this point in the history
Separate minmax
  • Loading branch information
fdobad authored Oct 9, 2024
2 parents f4cc62e + a0fe0f6 commit 323ba8c
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 32 deletions.
13 changes: 10 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Contact the fire2a team for access to Erico's and TechnoSylva's rasters. We will
2. Load a set of raster layers
3. Click on the "Pan European Proof of Concept" plugin icon
4. Configure for each layer/row (see details [below](#raster-configuration))
5. (TODO) Configure target resolution (optional)
5. Configure target raster creation (optional)
6. Buttons:
```
Reset: to clear the dialog, load another set of layers
Expand All @@ -57,8 +57,15 @@ For each available layer (must be local and written to disk) available configura
2. Weight attributes as spinbox & slider (they get adjusted to sum 100 at run time)
3. Resample method combobox selector (see details [below](#resampling-methods))
4. Utility function configuration, select between:
a. Min-Max scaling, with a invert values checkbox
b. Bi-Piecewise-Linear, with its two breakpoint setup as spinbox & slider (inverts when the second is lower than the first)
a. Min-Max scaling
b. Max-Min scaling, same but inverted
c. Bi-Piecewise-Linear Values, with its two breakpoint setup as data real values
d. Bi-Piecewise-Linear Percentage, with its two breakpoint setup percentage values from real data range (data.max - data.min)
5. Target raster creation setup:
- By default setup to create a "HD (1920x1080 pixels) image with each pixel representing an hectare (100x100m) pixel size" to speed up the processing
- Except when the zoom level is so high that the shown data is smaller than HD. In that case, the target raster will have the same resolution as the original raster in the actual viewport
- Currently the algorithm will get confused if the shown raster is not in a squared meters projection CRS
- Currently the datatype configuration is untested

### Resampling methods
Each target raster is expected to have billion pixels and the resampling method is crucial to the final result in a reasonable time. The following methods are available:
Expand Down
1 change: 1 addition & 0 deletions pan_batido/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ hasProcessingProvider=no
# Uncomment the following line and add your changelog:

changelog=
0.0.4 Separate utility functions for min-max & max-min, bi-piecewise-linear values & percentage
0.0.3 UI grouped in inputs and outputs, bug fix on dialog always calculating
0.0.2 Handling of nans and nodatas, qgis pseudocolor painting and no resample option
0.0.1 Initial: only happy path implemented! Don't edge case it!
Expand Down
66 changes: 53 additions & 13 deletions pan_batido/pan_batido.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,23 +276,43 @@ def run(self):
if 0 == ufdci:
new_data = min_max_scaling(
masked_data,
invert=dlg_row["minmax_invert"].isChecked(),
dtype=DATATYPES[data_type]["numpy"],
)
did_any = True
# bi_piecewise_linear
# max_min_scaling
elif 1 == ufdci:
new_data = max_min_scaling(
masked_data,
dtype=DATATYPES[data_type]["numpy"],
)
did_any = True
# bi_piecewise_linear_values
elif 2 == ufdci:
a = dlg_row["a_spinbox"].value()
b = dlg_row["b_spinbox"].value()
if a != b:
new_data = bi_piecewise_linear(
new_data = bi_piecewise_linear_values(
masked_data,
a,
b,
)
did_any = True
else:
qprint("a == b, skipping", level=Qgis.Warning)
qprint(f"bi_piecewise_linear_values {a} == {b}, skipping", level=Qgis.Warning)
continue
# bi_piecewise_linear_percentage
elif 3 == ufdci:
c = dlg_row["c_spinbox"].value()
d = dlg_row["d_spinbox"].value()
if c != d:
new_data = bi_piecewise_linear_percentage(
masked_data,
c,
d,
)
did_any = True
else:
qprint(f"bi_piecewise_linear_percentage {c} == {d}, skipping", level=Qgis.Warning)
continue
else:
from qgis.core import QgsException
Expand Down Expand Up @@ -322,20 +342,27 @@ def run(self):
qgis_paint(layer)


def min_max_scaling(data, invert=False, dtype=None):
def min_max_scaling(data, dtype=None):
if data.min() != data.max():
data = (data - data.min()) / (data.max() - data.min())
if invert:
if dtype == np.uint8:
return np.uint8(255 - data)
elif dtype == np.uint16:
return np.uint16(65535 - data)
else:
return 1 - data
if dtype == np.uint8:
return np.uint8(255 * data)
elif dtype == np.uint16:
return np.uint16(65535 * data)
return data


def bi_piecewise_linear(data, a, b):
def max_min_scaling(data, dtype=None):
if data.min() != data.max():
data = (data - data.max()) / (data.max() - data.min())
if dtype == np.uint8:
return np.uint8(255 * data)
elif dtype == np.uint16:
return np.uint16(65535 * data)
return data


def bi_piecewise_linear_values(data, a, b):
# linear scaling
data = (data - a) / (b - a)
# clip to [0, 1]
Expand All @@ -345,6 +372,19 @@ def bi_piecewise_linear(data, a, b):
return data


def bi_piecewise_linear_percentage(data, a, b):
# linear scaling
rela_delta = data.max() - data.min() / 100
real_a = rela_delta * a
real_b = rela_delta * b
data = (data - real_a) / (real_b - real_a)
# clip to [0, 1]
# TODO FIX DATATYPES? uint8, uint16 ?
data[data < 0] = 0
data[data > 1] = 1
return data


def get_sampled_raster_data(raster_path, extent, resolution=(1920, 1080), griora=0, gdt=7, layer_name=""):
"""Returns the data of the raster in the form of a numpy array, taken from the extent of the map canvas and resampled to resolution
Args:
Expand Down
59 changes: 43 additions & 16 deletions pan_batido/pan_batido_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@

from qgis.core import Qgis
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import (QCheckBox, QComboBox, QDialog, QGroupBox,
QDialogButtonBox, QGridLayout, QHBoxLayout,
QLabel, QSizePolicy, QSlider, QSpacerItem,
QSpinBox, QVBoxLayout, QWidget)
from qgis.PyQt.QtWidgets import (QCheckBox, QComboBox, QDialog,
QDialogButtonBox, QGridLayout, QGroupBox,
QHBoxLayout, QLabel, QSizePolicy, QSlider,
QSpacerItem, QSpinBox, QVBoxLayout, QWidget)
from qgis.utils import iface

from .config import DATATYPES, GRIORAS, qprint
Expand All @@ -47,7 +47,7 @@ def __init__(self, parent=None):
self.setLayout(self.verticalLayout)

# each row is a name | weight | resample | utility function
self.input_groupbox = QGroupBox('Input rasters')
self.input_groupbox = QGroupBox("Input rasters")
self.grid = QGridLayout()
self.grid.addWidget(QLabel("name"), 0, 0)
self.grid.addWidget(QLabel("weight"), 0, 1)
Expand Down Expand Up @@ -84,19 +84,25 @@ def __init__(self, parent=None):
ufunc_layout = QHBoxLayout()
ufunc_dropdown = QComboBox()
# NO REORDER:
ufunc_dropdown.addItems(["min-max", "bi-piecewise-linear"])
ufunc_dropdown.addItems(
["min-max", "max-min", "bi-piecewise-linear values", "bi-piecewise-linear percentage"]
)
# signal for hiding/showing each parameters
ufunc_dropdown.currentIndexChanged.connect(self.function_change)
# add id to the dropdown
ufunc_dropdown.row_id = i
ufunc_layout.addWidget(ufunc_dropdown)
# minmax parameters
cb = QCheckBox()
cb.row_id = i
cb.setText("Invert")
cb.setChecked(False)
cb.func_id = 0
ufunc_layout.addWidget(cb)
lbl1 = QLabel("")
lbl1.func_id = 0
lbl1.row_id = i
ufunc_layout.addWidget(lbl1)
# maxmin parameters
lbl2 = QLabel("")
lbl2.func_id = 1
lbl2.row_id = i
ufunc_layout.addWidget(lbl2)

# piecewise-linear parameters
# a
a_spinbox = QSpinBox()
Expand All @@ -108,7 +114,22 @@ def __init__(self, parent=None):
link_spinbox_slider(b_slider, b_spinbox)
for elto in [a_spinbox, a_slider, b_spinbox, b_slider]:
elto.row_id = i
elto.func_id = 1
elto.func_id = 2
elto.setVisible(False)
ufunc_layout.addWidget(elto)

# piecewise-linear parameters
# c
c_spinbox = QSpinBox()
c_slider = QSlider(Qt.Orientation.Horizontal)
link_spinbox_slider(c_slider, c_spinbox)
# d
d_spinbox = QSpinBox()
d_slider = QSlider(Qt.Orientation.Horizontal)
link_spinbox_slider(d_slider, d_spinbox)
for elto in [c_spinbox, c_slider, d_spinbox, d_slider]:
elto.row_id = i
elto.func_id = 3
elto.setVisible(False)
ufunc_layout.addWidget(elto)

Expand All @@ -120,11 +141,14 @@ def __init__(self, parent=None):
slider,
resample_dropdown,
ufunc_dropdown,
cb,
a_spinbox,
a_slider,
b_spinbox,
b_slider,
c_spinbox,
c_slider,
d_spinbox,
d_slider,
)
)

Expand All @@ -138,11 +162,14 @@ def __init__(self, parent=None):
"weight_slider": slider,
"resample_dropdown": resample_dropdown,
"ufunc_dropdown": ufunc_dropdown,
"minmax_invert": cb,
"a_spinbox": a_spinbox,
"a_slider": a_slider,
"b_spinbox": b_spinbox,
"b_slider": b_slider,
"c_spinbox": c_spinbox,
"c_slider": c_slider,
"d_spinbox": d_spinbox,
"d_slider": d_slider,
}
]
self.input_groupbox.setLayout(self.grid)
Expand All @@ -151,7 +178,7 @@ def __init__(self, parent=None):
self.verticalLayout.addItem(QSpacerItem(1, 1, QSizePolicy.Minimum, QSizePolicy.Expanding))

# target resolution x,y; pixel size, data type
self.target_groupbox = QGroupBox('Output configuration')
self.target_groupbox = QGroupBox("Output configuration")
self.target_layout = QGridLayout()
self.target_layout.addWidget(QLabel("width [px]:"), 0, 0)
self.resolution_x = QSpinBox()
Expand Down

0 comments on commit 323ba8c

Please sign in to comment.