diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP.vrt new file mode 100644 index 000000000000..0f68c6c8e697 --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP.vrt @@ -0,0 +1,13 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + 20,1 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats.vrt new file mode 100644 index 000000000000..cfb89cef412e --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats.vrt @@ -0,0 +1,31 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + 20,1 + + + + + 84 + 265 + 136.765 + 22.928470838676 + 100 + + + + + 94 + 275 + 146.765 + 22.928470838676 + 100 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_error.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_error.vrt new file mode 100644 index 000000000000..c19c934b35c4 --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_error.vrt @@ -0,0 +1,31 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + 20,1 + + + + + 84 + 265 + 136.765 + 22.928470838676 + 100 + + + + + 94 + 275 + 146.765 + 22.928470838676 + 100 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_missing_band.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_missing_band.vrt new file mode 100644 index 000000000000..e94bfcff09ab --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_missing_band.vrt @@ -0,0 +1,23 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + 20,1 + + + + + 84 + 265 + 136.765 + 22.928470838676 + 100 + + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_SOURCE.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_SOURCE.vrt new file mode 100644 index 000000000000..a46bdf91c5aa --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_SOURCE.vrt @@ -0,0 +1,13 @@ + + + ../byte.tif + + + + + + BandAffineCombination + 10,1 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_SOURCE_wrong_band_count.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_SOURCE_wrong_band_count.vrt new file mode 100644 index 000000000000..d5e8de291798 --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_FROM_SOURCE_wrong_band_count.vrt @@ -0,0 +1,14 @@ + + + ../byte.tif + + + + + + BandAffineCombination + 10,1 + 20,1 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED.vrt new file mode 100644 index 000000000000..564bb12dc1f1 --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED.vrt @@ -0,0 +1,12 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_invalid_type.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_invalid_type.vrt new file mode 100644 index 000000000000..9d172bab22c3 --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_invalid_type.vrt @@ -0,0 +1,12 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_non_numeric_count.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_non_numeric_count.vrt new file mode 100644 index 000000000000..d81e773da8f3 --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_non_numeric_count.vrt @@ -0,0 +1,12 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + + + diff --git a/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_too_large_count.vrt b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_too_large_count.vrt new file mode 100644 index 000000000000..11c1cef95eac --- /dev/null +++ b/autotest/gdrivers/data/vrt/processed_OutputBands_USER_PROVIDED_too_large_count.vrt @@ -0,0 +1,12 @@ + + + ../byte.tif + + + + + BandAffineCombination + 10,1 + + + diff --git a/autotest/gdrivers/vrtprocesseddataset.py b/autotest/gdrivers/vrtprocesseddataset.py index eea8907b0ced..fe076b8b8b08 100755 --- a/autotest/gdrivers/vrtprocesseddataset.py +++ b/autotest/gdrivers/vrtprocesseddataset.py @@ -1239,3 +1239,87 @@ def test_vrtprocesseddataset_serialize(tmp_vsimem): 11.5, 0.5, ] + + +############################################################################### +# Test OutputBands + + +def test_vrtprocesseddataset_OutputBands(): + + with gdal.Open("data/vrt/processed_OutputBands_FROM_LAST_STEP.vrt") as ds: + assert ds.RasterCount == 2 + assert (ds.GetRasterBand(1).GetMinimum(), ds.GetRasterBand(1).GetMaximum()) == ( + None, + None, + ) + assert (ds.GetRasterBand(2).GetMinimum(), ds.GetRasterBand(2).GetMaximum()) == ( + None, + None, + ) + assert ds.GetRasterBand(1).ComputeRasterMinMax() == (84, 265) + assert ds.GetRasterBand(2).ComputeRasterMinMax() == (94, 275) + + with gdal.Open( + "data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats.vrt" + ) as ds: + assert ds.RasterCount == 2 + assert (ds.GetRasterBand(1).GetMinimum(), ds.GetRasterBand(1).GetMaximum()) == ( + 84, + 265, + ) + assert (ds.GetRasterBand(2).GetMinimum(), ds.GetRasterBand(2).GetMaximum()) == ( + 94, + 275, + ) + + with gdal.Open( + "data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_missing_band.vrt" + ) as ds: + assert ds.RasterCount == 2 + assert (ds.GetRasterBand(1).GetMinimum(), ds.GetRasterBand(1).GetMaximum()) == ( + None, + None, + ) + assert (ds.GetRasterBand(2).GetMinimum(), ds.GetRasterBand(2).GetMaximum()) == ( + None, + None, + ) + assert ds.GetRasterBand(1).ComputeRasterMinMax() == (84, 265) + assert ds.GetRasterBand(2).ComputeRasterMinMax() == (94, 275) + + with pytest.raises(Exception, match="Argument coefficients_2 is missing"): + gdal.Open("data/vrt/processed_OutputBands_FROM_LAST_STEP_with_stats_error.vrt") + + with gdal.Open("data/vrt/processed_OutputBands_FROM_SOURCE.vrt") as ds: + assert ds.RasterCount == 1 + assert ds.GetRasterBand(1).ComputeRasterMinMax() == (84, 255) + + with pytest.raises( + Exception, + match="Final step expect 1 bands, but only 2 coefficient_XX are provided", + ): + gdal.Open("data/vrt/processed_OutputBands_FROM_SOURCE_wrong_band_count.vrt") + + with gdal.Open("data/vrt/processed_OutputBands_USER_PROVIDED.vrt") as ds: + assert ds.RasterCount == 1 + assert ds.GetRasterBand(1).DataType == gdal.GDT_Float32 + assert ds.GetRasterBand(1).ComputeRasterMinMax() == (84, 265) + + with pytest.raises( + Exception, + match="Invalid band count", + ): + gdal.Open("data/vrt/processed_OutputBands_USER_PROVIDED_too_large_count.vrt") + + with pytest.raises( + Exception, + match="Invalid value for OutputBands.count", + ): + gdal.Open("data/vrt/processed_OutputBands_USER_PROVIDED_non_numeric_count.vrt") + + with pytest.raises( + Exception, + match="Invalid value for OutputBands.dataType", + ): + gdal.Open("data/vrt/processed_OutputBands_USER_PROVIDED_invalid_type.vrt") diff --git a/doc/source/drivers/raster/vrt.rst b/doc/source/drivers/raster/vrt.rst index a5d16963ec40..7183c7929f35 100644 --- a/doc/source/drivers/raster/vrt.rst +++ b/doc/source/drivers/raster/vrt.rst @@ -186,7 +186,7 @@ VRTRasterBand The attributes for VRTRasterBand are: - **dataType** (optional): type of the pixel data associated with this band (use - names Byte, UInt16, Int16, UInt32, Int32, Float32, Float64, CInt16, CInt32, CFloat32 or CFloat64). + names Byte, Int8, UInt16, Int16, UInt32, Int32, UInt64, Int64, Float32, Float64, CInt16, CInt32, CFloat32 or CFloat64). If not specified, defaults to 1 - **band** (optional): band number this element represents (1 based). diff --git a/doc/source/drivers/raster/vrt_processed_dataset.rst b/doc/source/drivers/raster/vrt_processed_dataset.rst index 35b4d215ecb7..085b3c297a18 100644 --- a/doc/source/drivers/raster/vrt_processed_dataset.rst +++ b/doc/source/drivers/raster/vrt_processed_dataset.rst @@ -119,9 +119,7 @@ The ``VRTDataset`` root element must have a ``subClass="VRTProcessedDataset"`` a The following child elements of ``VRTDataset`` may be defined: ``SRS``, ``GeoTransform``, ``Metadata``. If they are not explicitly set, they are inferred from the input dataset. -``VRTRasterBand`` elements may be explicitly defined, in particular if the data type of the virtual dataset after all processing steps is different from the input one, or if the number of output bands is different from the number of input bands. If there is no explicit ``VRTRasterBand`` element, the number and data types of input bands are used implicitly. When explicitly defined, ``VRTRasterBand`` elements must have a ``subClass="VRTProcessedRasterBand"`` attribute. -` -It must also have the 2 following child elements: +The ``VRTDataset`` root element must also have the 2 following child elements: - ``Input``, which must have one and only one of the following ``SourceFilename`` or ``VRTDataset`` as child elements, to define the input dataset to which to apply the processing steps. @@ -132,6 +130,48 @@ The value of ``Algorithm`` must be a registered VRTProcessedDataset function. At A ``Step`` will generally have one or several ``Argument`` child elements, some of them being required, others optional. Consult the documentation of each algorithm. +Starting with GDAL 3.11, a ``OutputBands`` element can be +defined as a child element of ``VRTDataset``, with the following 2 attributes: + +* ``count`` whose value can be ``FROM_SOURCE`` to indicate that the output band + count must be the same as the number of bands of the input dataset, + ``FROM_LAST_STEP`` to indicate that it must be the number of output bands + returned by the initialization function of the last step, or an integer value. + +* ``dataType`` whose value can be ``FROM_SOURCE`` to indicate that the output band + data type must be the same as one of the input dataset, + ``FROM_LAST_STEP`` to indicate that it must be the one returned by the + initialization function of the last step, or a value among + Byte, Int8, UInt16, Int16, UInt32, Int32, UInt64, Int64, Float32, Float64, CInt16, CInt32, CFloat32 or CFloat64 + +Example: + +.. code-block:: xml + + + + source.tif + + + ... + + + +If ``OutputBands`` is omitted, + +* if there are explicit ``VRTRasterBand`` elements, they must have a + ``subClass="VRTProcessedRasterBand"`` attribute + +* it there are no explicit ``VRTRasterBand`` elements, the number and data types + of input bands are used implicitly. + +Both ``OutputBands`` and ``VRTRasterBand`` elements may be defined. The information +specified by ``OutputBands`` will be used in priority, and ``VRTRasterBand`` elements +will be used only if they are compatible with the band count and data type specified +through ``OutputBands``. A situation where ``OutputBands`` and ``VRTRasterBand`` elements +are both found is for example when computing statistics on a .vrt file with only +``OutputBands`` initially set. + LocalScaleOffset algorithm -------------------------- diff --git a/frmts/vrt/data/gdalvrt.xsd b/frmts/vrt/data/gdalvrt.xsd index 90dbcf33670b..ad9d0aa63902 100644 --- a/frmts/vrt/data/gdalvrt.xsd +++ b/frmts/vrt/data/gdalvrt.xsd @@ -52,6 +52,11 @@ Allowed only if subClass="VRTProcessedDataset" + + + Allowed only if subClass="VRTProcessedDataset" + + Allowed only if subClass="VRTProcessedDataset" @@ -195,6 +200,26 @@ + + + + + FROM_SOURCE, FROM_LAST_STEP or positive integer value. + If not specified, FROM_SOURCE is the default if there are no VRTRasterBand elements. + Otherwise, if there are VRTRasterBand elements, they are used. + + + + + + FROM_SOURCE, FROM_LAST_STEP or one of the allowed values of DataTypeType. + If not specified, FROM_SOURCE is the default if there are no VRTRasterBand elements. + Otherwise, if there are VRTRasterBand elements, they are used. + + + + + diff --git a/frmts/vrt/vrtdataset.h b/frmts/vrt/vrtdataset.h index 1189667ff5a2..bdb148343f1d 100644 --- a/frmts/vrt/vrtdataset.h +++ b/frmts/vrt/vrtdataset.h @@ -733,6 +733,28 @@ class VRTProcessedDataset final : public VRTDataset //! Output buffer of a processing step std::vector m_abyOutput{}; + //! Provenance of OutputBands.count and OutputBands.dataType + enum class ValueProvenance + { + FROM_VRTRASTERBAND, + FROM_SOURCE, + FROM_LAST_STEP, + USER_PROVIDED, + }; + + //! Provenance of OutputBands.count attribute + ValueProvenance m_outputBandCountProvenance = ValueProvenance::FROM_SOURCE; + + //! Value of OutputBands.count attribute if m_outputBandCountProvenance = USER_PROVIDED + int m_outputBandCountValue = 0; + + //! Provenance of OutputBands.dataType attribute + ValueProvenance m_outputBandDataTypeProvenance = + ValueProvenance::FROM_SOURCE; + + //! Value of OutputBands.dataType attribute if m_outputBandDataTypeProvenance = USER_PROVIDED + GDALDataType m_outputBandDataTypeValue = GDT_Unknown; + CPLErr Init(const CPLXMLNode *, const char *, const VRTProcessedDataset *poParentDS, GDALDataset *poParentSrcDS, int iOvrLevel); diff --git a/frmts/vrt/vrtprocesseddataset.cpp b/frmts/vrt/vrtprocesseddataset.cpp index b8cd27b76915..27ee1a437f69 100644 --- a/frmts/vrt/vrtprocesseddataset.cpp +++ b/frmts/vrt/vrtprocesseddataset.cpp @@ -335,18 +335,80 @@ CPLErr VRTProcessedDataset::Init(const CPLXMLNode *psTree, static_cast(poParentDS->GetRasterYSize()) / nRasterYSize; } - // Create bands automatically from source dataset if not explicitly defined - // in VRT. - if (!CPLGetXMLNode(psTree, "VRTRasterBand")) + const CPLXMLNode *psOutputBands = CPLGetXMLNode(psTree, "OutputBands"); + if (psOutputBands) { - for (int i = 0; i < m_poSrcDS->GetRasterCount(); ++i) + if (const char *pszCount = + CPLGetXMLValue(psOutputBands, "count", nullptr)) { - const auto poSrcBand = m_poSrcDS->GetRasterBand(i + 1); - auto poBand = new VRTProcessedRasterBand( - this, i + 1, poSrcBand->GetRasterDataType()); - poBand->CopyCommonInfoFrom(poSrcBand); - SetBand(i + 1, poBand); + if (EQUAL(pszCount, "FROM_LAST_STEP")) + { + m_outputBandCountProvenance = ValueProvenance::FROM_LAST_STEP; + } + else if (!EQUAL(pszCount, "FROM_SOURCE")) + { + if (CPLGetValueType(pszCount) == CPL_VALUE_INTEGER) + { + m_outputBandCountProvenance = + ValueProvenance::USER_PROVIDED; + m_outputBandCountValue = atoi(pszCount); + if (!GDALCheckBandCount(m_outputBandCountValue, + /* bIsZeroAllowed = */ false)) + { + // Error emitted by above function + return CE_Failure; + } + } + else + { + CPLError(CE_Failure, CPLE_AppDefined, + "Invalid value for OutputBands.count"); + return CE_Failure; + } + } } + + if (const char *pszType = + CPLGetXMLValue(psOutputBands, "dataType", nullptr)) + { + if (EQUAL(pszType, "FROM_LAST_STEP")) + { + m_outputBandDataTypeProvenance = + ValueProvenance::FROM_LAST_STEP; + } + else if (!EQUAL(pszType, "FROM_SOURCE")) + { + m_outputBandDataTypeProvenance = ValueProvenance::USER_PROVIDED; + m_outputBandDataTypeValue = GDALGetDataTypeByName(pszType); + if (m_outputBandDataTypeValue == GDT_Unknown) + { + CPLError(CE_Failure, CPLE_AppDefined, + "Invalid value for OutputBands.dataType"); + return CE_Failure; + } + } + } + } + else if (CPLGetXMLNode(psTree, "VRTRasterBand")) + { + m_outputBandCountProvenance = ValueProvenance::FROM_VRTRASTERBAND; + m_outputBandDataTypeProvenance = ValueProvenance::FROM_VRTRASTERBAND; + } + + int nOutputBandCount = 0; + switch (m_outputBandCountProvenance) + { + case ValueProvenance::USER_PROVIDED: + nOutputBandCount = m_outputBandCountValue; + break; + case ValueProvenance::FROM_SOURCE: + nOutputBandCount = m_poSrcDS->GetRasterCount(); + break; + case ValueProvenance::FROM_VRTRASTERBAND: + nOutputBandCount = nBands; + break; + case ValueProvenance::FROM_LAST_STEP: + break; } const CPLXMLNode *psProcessingSteps = @@ -410,14 +472,39 @@ CPLErr VRTProcessedDataset::Init(const CPLXMLNode *psTree, { // Initialize adfOutNoData with nodata value of *output* bands // for final step - for (int i = 1; i <= nBands; ++i) + if (m_outputBandCountProvenance == + ValueProvenance::FROM_VRTRASTERBAND) + { + for (int i = 1; i <= nBands; ++i) + { + int bHasVal = FALSE; + const double dfVal = + GetRasterBand(i)->GetNoDataValue(&bHasVal); + adfOutNoData.emplace_back( + bHasVal ? dfVal + : std::numeric_limits::quiet_NaN()); + } + } + else if (m_outputBandCountProvenance == + ValueProvenance::FROM_SOURCE) + { + for (int i = 1; i <= m_poSrcDS->GetRasterCount(); ++i) + { + int bHasVal = FALSE; + const double dfVal = + m_poSrcDS->GetRasterBand(i)->GetNoDataValue( + &bHasVal); + adfOutNoData.emplace_back( + bHasVal ? dfVal + : std::numeric_limits::quiet_NaN()); + } + } + else if (m_outputBandCountProvenance == + ValueProvenance::USER_PROVIDED) { - int bHasVal = FALSE; - const double dfVal = - GetRasterBand(i)->GetNoDataValue(&bHasVal); - adfOutNoData.emplace_back( - bHasVal ? dfVal - : std::numeric_limits::quiet_NaN()); + adfOutNoData.resize( + m_outputBandCountValue, + std::numeric_limits::quiet_NaN()); } } if (!ParseStep(psStep, bIsFinalStep, eCurrentDT, nCurrentBandCount, @@ -434,14 +521,73 @@ CPLErr VRTProcessedDataset::Init(const CPLXMLNode *psTree, return CE_Failure; } - if (nCurrentBandCount != nBands) + if (m_outputBandCountProvenance == ValueProvenance::FROM_LAST_STEP) + { + nOutputBandCount = nCurrentBandCount; + } + else if (nOutputBandCount != nCurrentBandCount) { + // Should not happen frequently as pixel init functions are expected + // to validate that they can accept the number of output bands provided + // to them CPLError(CE_Failure, CPLE_AppDefined, "Number of output bands of last step is not consistent with " "number of VRTProcessedRasterBand's"); return CE_Failure; } + if (m_outputBandDataTypeProvenance == ValueProvenance::FROM_LAST_STEP) + { + m_outputBandDataTypeValue = eCurrentDT; + } + + if (nBands != 0 && + (nBands != nOutputBandCount || + (m_outputBandDataTypeProvenance == ValueProvenance::FROM_LAST_STEP && + m_outputBandDataTypeValue != papoBands[0]->GetRasterDataType()))) + { + for (int i = 0; i < nBands; ++i) + delete papoBands[i]; + CPLFree(papoBands); + papoBands = nullptr; + nBands = 0; + } + + const auto GetOutputBandType = [this, eCurrentDT](GDALDataType eSourceDT) + { + if (m_outputBandDataTypeProvenance == ValueProvenance::FROM_LAST_STEP) + return eCurrentDT; + else if (m_outputBandDataTypeProvenance == + ValueProvenance::USER_PROVIDED) + return m_outputBandDataTypeValue; + else + return eSourceDT; + }; + + if (m_outputBandCountProvenance == ValueProvenance::FROM_SOURCE) + { + for (int i = 0; i < m_poSrcDS->GetRasterCount(); ++i) + { + const auto poSrcBand = m_poSrcDS->GetRasterBand(i + 1); + const GDALDataType eOutputBandType = + GetOutputBandType(poSrcBand->GetRasterDataType()); + auto poBand = + new VRTProcessedRasterBand(this, i + 1, eOutputBandType); + poBand->CopyCommonInfoFrom(poSrcBand); + SetBand(i + 1, poBand); + } + } + else if (m_outputBandCountProvenance != ValueProvenance::FROM_VRTRASTERBAND) + { + const GDALDataType eOutputBandType = GetOutputBandType(eInDT); + for (int i = 0; i < nOutputBandCount; ++i) + { + auto poBand = + new VRTProcessedRasterBand(this, i + 1, eOutputBandType); + SetBand(i + 1, poBand); + } + } + if (nBands > 1) SetMetadataItem("INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE"); @@ -722,13 +868,13 @@ bool VRTProcessedDataset::ParseStep(const CPLXMLNode *psStep, bool bIsFinalStep, if (oFunc.pfnInit) { double *padfOutNoData = nullptr; - if (bIsFinalStep) + if (bIsFinalStep && !adfOutNoData.empty()) { - oStep.nOutBands = nBands; - padfOutNoData = - static_cast(CPLMalloc(nBands * sizeof(double))); - CPLAssert(adfOutNoData.size() == static_cast(nBands)); - memcpy(padfOutNoData, adfOutNoData.data(), nBands * sizeof(double)); + oStep.nOutBands = static_cast(adfOutNoData.size()); + padfOutNoData = static_cast( + CPLMalloc(adfOutNoData.size() * sizeof(double))); + memcpy(padfOutNoData, adfOutNoData.data(), + adfOutNoData.size() * sizeof(double)); } else { @@ -754,8 +900,8 @@ bool VRTProcessedDataset::ParseStep(const CPLXMLNode *psStep, bool bIsFinalStep, if (padfOutNoData) { - adfOutNoData = - std::vector(padfOutNoData, padfOutNoData + nBands); + adfOutNoData = std::vector(padfOutNoData, + padfOutNoData + oStep.nOutBands); } else {