Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warper: add a EXCLUDED_VALUES warping option, and use it in gdal2tiles #9631

Merged
merged 2 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions alg/gdalwarper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,24 @@ CPLErr GDALWarpDstAlphaMasker(void *pMaskFuncArg, int nBandCount,
* an explicit source and target SRS.</li>
* <li>MULT_FACTOR_VERTICAL_SHIFT: Multiplication factor for the vertical
* shift. Default 1.0</li>
*
* <li>EXCLUDED_VALUES: (GDAL >= 3.9) Comma-separated tuple of values
* (thus typically "R,G,B"), that are ignored as contributing source
* pixels during resampling. The number of values in the tuple must be the same
* as the number of bands, excluding the alpha band.
* Several tuples of excluded values may be specified using the
* "(R1,G1,B2),(R2,G2,B2)" syntax.
* Only taken into account by Average currently.
* This concept is a bit similar to nodata/alpha, but the main difference is
* that pixels matching one of the excluded value tuples are still considered
* as valid, when determining the target pixel validity/density.
* </li>
*
* <li>EXCLUDED_VALUES_PCT_THRESHOLD=[0-100]: (GDAL >= 3.9) Minimum percentage
* of source pixels that must be set at one of the EXCLUDED_VALUES to cause
* the excluded value, that is in majority among source pixels, to be used as the
* target pixel value. Default value is 50 (%)</li>
*
* </ul>
*/

Expand Down
6 changes: 6 additions & 0 deletions alg/gdalwarper.h
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,12 @@ class CPL_DLL GDALWarpKernel
bool bApplyVerticalShift = false;

double dfMultFactorVerticalShift = 1.0;

// Tuples of values (e.g. "<R>,<G>,<B>" or "(<R1>,<G1>,<B1>),(<R2>,<G2>,<B2>)") that must
// be ignored as contributing source pixels during resampling. Only taken into account by
// Average currently
std::vector<std::vector<double>> m_aadfExcludedValues{};

/*! @endcond */

GDALWarpKernel();
Expand Down
199 changes: 185 additions & 14 deletions alg/gdalwarpkernel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,37 @@ CPLErr GDALWarpKernel::Validate()
return CE_Failure;
}

// Tuples of values (e.g. "<R>,<G>,<B>" or "(<R1>,<G1>,<B1>),(<R2>,<G2>,<B2>)") that must
// be ignored as contributing source pixels during resampling. Only taken into account by
// Average currently
const char *pszExcludedValues =
CSLFetchNameValue(papszWarpOptions, "EXCLUDED_VALUES");
if (pszExcludedValues)
{
const CPLStringList aosTokens(
CSLTokenizeString2(pszExcludedValues, "(,)", 0));
if ((aosTokens.size() % nBands) != 0)
{
CPLError(CE_Failure, CPLE_AppDefined,
"EXCLUDED_VALUES should contain one or several tuples of "
"%d values formatted like <R>,<G>,<B> or "
"(<R1>,<G1>,<B1>),(<R2>,<G2>,<B2>) if there are multiple "
"tuples",
nBands);
return CE_Failure;
}
std::vector<double> adfTuple;
for (int i = 0; i < aosTokens.size(); ++i)
{
adfTuple.push_back(CPLAtof(aosTokens[i]));
if (((i + 1) % nBands) == 0)
{
m_aadfExcludedValues.push_back(adfTuple);
adfTuple.clear();
}
}
}

return CE_None;
}

Expand Down Expand Up @@ -6549,6 +6580,11 @@ static void GWKAverageOrModeThread(void *pData)
const double dfErrorThreshold = CPLAtof(
CSLFetchNameValueDef(poWK->papszWarpOptions, "ERROR_THRESHOLD", "0"));

const double dfExcludedValuesThreshold =
CPLAtof(CSLFetchNameValueDef(poWK->papszWarpOptions,
"EXCLUDED_VALUES_PCT_THRESHOLD", "50")) /
100.0;

const int nXMargin =
2 * std::max(1, static_cast<int>(std::ceil(1. / poWK->dfXScale)));
const int nYMargin =
Expand Down Expand Up @@ -6689,13 +6725,161 @@ static void GWKAverageOrModeThread(void *pData)
if (iSrcYMin == iSrcYMax && iSrcYMax < nSrcYSize)
iSrcYMax++;

#define COMPUTE_WEIGHT_Y(iSrcY) \
((iSrcY == iSrcYMin) \
? ((iSrcYMin + 1 == iSrcYMax) ? 1.0 : 1 - (dfYMin - iSrcYMin)) \
: (iSrcY + 1 == iSrcYMax) ? 1 - (iSrcYMax - dfYMax) \
: 1.0)

#define COMPUTE_WEIGHT(iSrcX, dfWeightY) \
((iSrcX == iSrcXMin) ? ((iSrcXMin + 1 == iSrcXMax) \
? dfWeightY \
: dfWeightY * (1 - (dfXMin - iSrcXMin))) \
: (iSrcX + 1 == iSrcXMax) ? dfWeightY * (1 - (iSrcXMax - dfXMax)) \
: dfWeightY)

bool bDone = false;

// Special Average mode where we process all bands together,
// to avoid averaging tuples that match an entry of m_aadfExcludedValues
if (nAlgo == GWKAOM_Average &&
!poWK->m_aadfExcludedValues.empty() &&
!poWK->bApplyVerticalShift && !bIsComplex)
{
double dfTotalWeightInvalid = 0.0;
double dfTotalWeightExcluded = 0.0;
double dfTotalWeightRegular = 0.0;
std::vector<double> adfValueReal(poWK->nBands, 0);
std::vector<double> adfValueAveraged(poWK->nBands, 0);
std::vector<int> anCountExcludedValues(
poWK->m_aadfExcludedValues.size(), 0);

for (int iSrcY = iSrcYMin; iSrcY < iSrcYMax; iSrcY++)
{
const double dfWeightY = COMPUTE_WEIGHT_Y(iSrcY);
iSrcOffset =
iSrcXMin + static_cast<GPtrDiff_t>(iSrcY) * nSrcXSize;
for (int iSrcX = iSrcXMin; iSrcX < iSrcXMax;
iSrcX++, iSrcOffset++)
{
if (bWrapOverX)
iSrcOffset =
(iSrcX % nSrcXSize) +
static_cast<GPtrDiff_t>(iSrcY) * nSrcXSize;

if (poWK->panUnifiedSrcValid != nullptr &&
!CPLMaskGet(poWK->panUnifiedSrcValid, iSrcOffset))
{
continue;
}

const double dfWeight =
COMPUTE_WEIGHT(iSrcX, dfWeightY);
if (dfWeight <= 0)
continue;

bool bAllValid = true;
for (int iBand = 0; iBand < poWK->nBands; iBand++)
{
double dfBandDensity = 0;
double dfValueImagTmp = 0;
if (!(GWKGetPixelValue(
poWK, iBand, iSrcOffset, &dfBandDensity,
&adfValueReal[iBand], &dfValueImagTmp) &&
dfBandDensity > BAND_DENSITY_THRESHOLD))
{
bAllValid = false;
break;
}
}

if (!bAllValid)
{
dfTotalWeightInvalid += dfWeight;
continue;
}

bool bExcludedValueFound = false;
for (size_t i = 0;
i < poWK->m_aadfExcludedValues.size(); ++i)
{
if (poWK->m_aadfExcludedValues[i] == adfValueReal)
{
bExcludedValueFound = true;
++anCountExcludedValues[i];
dfTotalWeightExcluded += dfWeight;
break;
}
}
if (!bExcludedValueFound)
{
// Weighted incremental algorithm mean
// Cf https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Weighted_incremental_algorithm
dfTotalWeightRegular += dfWeight;
for (int iBand = 0; iBand < poWK->nBands; iBand++)
{
adfValueAveraged[iBand] +=
(dfWeight / dfTotalWeightRegular) *
(adfValueReal[iBand] -
adfValueAveraged[iBand]);
}
}
}
}

const double dfTotalWeight = dfTotalWeightInvalid +
dfTotalWeightExcluded +
dfTotalWeightRegular;
if (dfTotalWeightExcluded > 0 &&
dfTotalWeightExcluded >=
dfExcludedValuesThreshold * dfTotalWeight)
{
// Find the most represented excluded value tuple
size_t iExcludedValue = 0;
int nExcludedValueCount = 0;
for (size_t i = 0; i < poWK->m_aadfExcludedValues.size();
++i)
{
if (anCountExcludedValues[i] > nExcludedValueCount)
{
iExcludedValue = i;
nExcludedValueCount = anCountExcludedValues[i];
}
}

bHasFoundDensity = true;

for (int iBand = 0; iBand < poWK->nBands; iBand++)
{
GWKSetPixelValue(
poWK, iBand, iDstOffset, /* dfBandDensity = */ 1.0,
poWK->m_aadfExcludedValues[iExcludedValue][iBand],
0);
}
}
else if (dfTotalWeightRegular > 0)
{
bHasFoundDensity = true;

for (int iBand = 0; iBand < poWK->nBands; iBand++)
{
GWKSetPixelValue(poWK, iBand, iDstOffset,
/* dfBandDensity = */ 1.0,
adfValueAveraged[iBand], 0);
}
}

// Skip below loop on bands
bDone = true;
}

/* ====================================================================
*/
/* Loop processing each band. */
/* ====================================================================
*/

for (int iBand = 0; iBand < poWK->nBands; iBand++)
for (int iBand = 0; !bDone && iBand < poWK->nBands; iBand++)
{
double dfBandDensity = 0.0;
double dfValueReal = 0.0;
Expand All @@ -6711,19 +6895,6 @@ static void GWKAverageOrModeThread(void *pData)

// Loop over source lines and pixels - 3 possible algorithms.

#define COMPUTE_WEIGHT_Y(iSrcY) \
((iSrcY == iSrcYMin) \
? ((iSrcYMin + 1 == iSrcYMax) ? 1.0 : 1 - (dfYMin - iSrcYMin)) \
: (iSrcY + 1 == iSrcYMax) ? 1 - (iSrcYMax - dfYMax) \
: 1.0)

#define COMPUTE_WEIGHT(iSrcX, dfWeightY) \
((iSrcX == iSrcXMin) ? ((iSrcXMin + 1 == iSrcXMax) \
? dfWeightY \
: dfWeightY * (1 - (dfXMin - iSrcXMin))) \
: (iSrcX + 1 == iSrcXMax) ? dfWeightY * (1 - (iSrcXMax - dfXMax)) \
: dfWeightY)

// poWK->eResample == GRA_Average.
if (nAlgo == GWKAOM_Average)
{
Expand Down
75 changes: 75 additions & 0 deletions autotest/alg/warp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1740,3 +1740,78 @@ def test_non_square():
assert res.ymax == pytest.approx(30.25)
assert res.xmin == pytest.approx(9.9)
assert res.xmax == pytest.approx(10.5)


###############################################################################
# Test EXCLUDED_VALUES warping option with average resampling


def test_warp_average_excluded_values():

src_ds = gdal.GetDriverByName("MEM").Create("", 2, 2, 3, gdal.GDT_Byte)
src_ds.GetRasterBand(1).WriteRaster(
0, 0, 2, 2, struct.pack("B" * 4, 10, 20, 30, 40)
)
src_ds.GetRasterBand(2).WriteRaster(
0, 0, 2, 2, struct.pack("B" * 4, 11, 21, 31, 41)
)
src_ds.GetRasterBand(3).WriteRaster(
0, 0, 2, 2, struct.pack("B" * 4, 12, 22, 32, 42)
)
src_ds.SetGeoTransform([1, 1, 0, 1, 0, 1])

with pytest.raises(
Exception,
match="EXCLUDED_VALUES should contain one or several tuples of 3 values",
):
out_ds = gdal.Warp(
"", src_ds, options="-of MEM -ts 1 1 -r average -wo EXCLUDED_VALUES=30,31"
)

# The excluded value is just ignored in contributing source pixels that are average, as it represents only 25% of contributing pixels
out_ds = gdal.Warp(
"", src_ds, options="-of MEM -ts 1 1 -r average -wo EXCLUDED_VALUES=(30,31,32)"
)
assert struct.unpack("B" * 3, out_ds.ReadRaster()) == (
(10 + 20 + 40) // 3,
(11 + 21 + 41) // 3,
(12 + 22 + 42) // 3,
)

# The excluded value is selected because its contributing 25% is >= 0%
out_ds = gdal.Warp(
"",
src_ds,
options="-of MEM -ts 1 1 -r average -wo EXCLUDED_VALUES=(30,31,32) -wo EXCLUDED_VALUES_PCT_THRESHOLD=0",
)
assert struct.unpack("B" * 3, out_ds.ReadRaster()) == (30, 31, 32)

# The excluded value is selected because its contributing 25% is >= 24%
out_ds = gdal.Warp(
"",
src_ds,
options="-of MEM -ts 1 1 -r average -wo EXCLUDED_VALUES=(30,31,32) -wo EXCLUDED_VALUES_PCT_THRESHOLD=24",
)
assert struct.unpack("B" * 3, out_ds.ReadRaster()) == (30, 31, 32)

# The excluded value is selected because its contributing 25% is < 26%
out_ds = gdal.Warp(
"",
src_ds,
options="-of MEM -ts 1 1 -r average -wo EXCLUDED_VALUES=(30,31,32) -wo EXCLUDED_VALUES_PCT_THRESHOLD=26",
)
assert struct.unpack("B" * 3, out_ds.ReadRaster()) == (
(10 + 20 + 40) // 3,
(11 + 21 + 41) // 3,
(12 + 22 + 42) // 3,
)

# No match of excluded value
out_ds = gdal.Warp(
"", src_ds, options="-of MEM -ts 1 1 -r average -wo EXCLUDED_VALUES=(30,31,0)"
)
assert struct.unpack("B" * 3, out_ds.ReadRaster()) == (
(10 + 20 + 30 + 40) // 4,
(11 + 21 + 31 + 41) // 4,
(12 + 22 + 32 + 42) // 4,
)
Loading
Loading