Use a stretch renderer to enhance the visual contrast of raster data for analysis.

Use case
An appropriate stretch renderer can enhance the contrast of raster imagery, allowing the user to control how their data is displayed for efficient imagery analysis.
How to use the sample
Choose one of the stretch parameter types:
- Standard deviation - a linear stretch defined by the standard deviation of the pixel values
- Min-max - a linear stretch based on minimum and maximum pixel values
- Percent clip - a linear stretch between the defined percent clip minimum and percent clip maximum pixel values
Then configure the parameters.
How it works
- Create a
Rasterfrom a raster file usingRaster.createWithPath. - Create a
RasterLayerfrom theRaster. - Add the layer to the map’s operational layers.
- Create a
StretchRenderer, specifying the stretch parameters and other properties. - Set the renderer on the layer using
rasterLayer.renderer(...).
Relevant API
- ColorRamp
- MinMaxStretchParameters
- PercentClipStretchParameters
- Raster
- RasterLayer
- StandardDeviationStretchParameters
- StretchParameters
- StretchRenderer
Offline data
This sample uses the Shasta raster file.
About the data
This sample uses a raster imagery tile of an area of forested mountainous terrain and rivers.
Additional information
See Stretch function in the ArcGIS Pro documentation for more information about the types of stretches that can be performed.
Tags
analysis, deviation, histogram, imagery, interpretation, min-max, percent clip, pixel, raster, stretch, symbology, visualization
Sample Code
/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */package com.esri.arcgismaps.sample.applystretchrenderer
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), getString(R.string.apply_stretch_renderer_app_name), listOf( "https://www.arcgis.com/home/item.html?id=7c4c679ab06a4df19dc497f577f111bd" ) ) }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.applystretchrenderer
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.applystretchrenderer.screens.ApplyStretchRendererScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
setContent { SampleAppTheme { ApplyStretchRendererApp() } } }
@Composable private fun ApplyStretchRendererApp() { Surface(color = MaterialTheme.colorScheme.background) { ApplyStretchRendererScreen( sampleName = getString(R.string.apply_stretch_renderer_app_name) ) } }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.applystretchrenderer.components
import android.app.Applicationimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.layers.RasterLayerimport com.arcgismaps.mapping.symbology.raster.MinMaxStretchParametersimport com.arcgismaps.mapping.symbology.raster.PercentClipStretchParametersimport com.arcgismaps.mapping.symbology.raster.StandardDeviationStretchParametersimport com.arcgismaps.mapping.symbology.raster.StretchParametersimport com.arcgismaps.mapping.symbology.raster.StretchRendererimport com.arcgismaps.raster.Rasterimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.applystretchrenderer.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launchimport java.io.File
private const val DEFAULT_MIN = 10.0private const val DEFAULT_MAX = 150.0private const val DEFAULT_PERCENT_MIN = 0.0private const val DEFAULT_PERCENT_MAX = 50.0private const val DEFAULT_STD_DEVIATION_FACTOR = 0.5
/** * ViewModel for the "Apply stretch renderer" sample. */class ApplyStretchRendererViewModel(private val app: Application) : AndroidViewModel(app) {
// The map with imagery basemap style val arcGISMap: ArcGISMap = ArcGISMap(BasemapStyle.ArcGISImageryStandard)
// MapViewProxy used to set viewpoint after layer loads val mapViewProxy = MapViewProxy()
// Message dialog view model for error handling val messageDialogVM = MessageDialogViewModel()
// Provision path for local offline resources private val provisionPath by lazy { app.getExternalFilesDir(null)?.path.toString() + File.separator + app.getString(R.string.apply_stretch_renderer_app_name) }
// The raster data (raster-file/Shasta.tif) should be downloaded to external storage on launch private val raster by lazy { val rasterFile = File(provisionPath, "raster-file${File.separator}Shasta.tif") Raster.createWithPath(rasterFile.path) }
// The raster layer to which the stretch renderer will be applied private val rasterLayer = RasterLayer(raster)
// Stretch type options for UI val stretchTypeOptions = listOf("MinMax", "Percent Clip", "Std Deviation")
// Current selected stretch type private val _selectedStretchType = MutableStateFlow(StretchType.MinMax) val selectedStretchType = _selectedStretchType.asStateFlow()
// Min-Max parameters (values represent pixel value range) private val _minValue = MutableStateFlow(DEFAULT_MIN) val minValue = _minValue.asStateFlow()
private val _maxValue = MutableStateFlow(DEFAULT_MAX) val maxValue = _maxValue.asStateFlow()
// Percent clip parameters (values represent percent 0..100) private val _percentMin = MutableStateFlow(DEFAULT_PERCENT_MIN) val percentMin = _percentMin.asStateFlow()
private val _percentMax = MutableStateFlow(DEFAULT_PERCENT_MAX) val percentMax = _percentMax.asStateFlow()
// Standard deviation factor (typical range 0.25..4.0) private val _stdDeviationFactor = MutableStateFlow(DEFAULT_STD_DEVIATION_FACTOR) val stdDeviationFactor = _stdDeviationFactor.asStateFlow()
init { viewModelScope.launch { rasterLayer.load().onSuccess { arcGISMap.operationalLayers.add(rasterLayer) }.onFailure { return@launch messageDialogVM.showMessageDialog(it) }
arcGISMap.load().onFailure { return@launch messageDialogVM.showMessageDialog(it) }
rasterLayer.fullExtent?.center?.let { center -> mapViewProxy.setViewpoint(Viewpoint(center = center, scale = 80_000.0)) } updateRenderer() } }
/** Update the current stretch type by index from [stretchTypeOptions]. */ fun updateStretchTypeByIndex(index: Int) { val safeIndex = index.coerceIn(0, stretchTypeOptions.lastIndex) _selectedStretchType.value = when (safeIndex) { 0 -> StretchType.MinMax 1 -> StretchType.PercentClip else -> StretchType.StandardDeviation } updateRenderer() }
/** Update MinMax minimum value (clamped to 0..(max-1)). */ fun updateMinValue(value: Double) { val max = _maxValue.value _minValue.value = value.coerceIn(DEFAULT_MIN, max - 1.0) updateRenderer() }
/** Update MinMax maximum value (clamped to (min+1)..255). */ fun updateMaxValue(value: Double) { val min = _minValue.value _maxValue.value = value.coerceIn(min + 1.0, 255.0) updateRenderer() }
/** Update Percent Clip minimum percent (clamped to 0..percentMax). */ fun updatePercentMin(value: Double) { val max = _percentMax.value _percentMin.value = value.coerceIn(DEFAULT_MIN, max) updateRenderer() }
/** Update Percent Clip maximum percent (clamped to percentMin..100). */ fun updatePercentMax(value: Double) { val min = _percentMin.value _percentMax.value = value.coerceIn(min, 100.0) updateRenderer() }
/** Update Standard Deviation factor (clamped to 0.25..4.0). */ fun updateStdDeviationFactor(value: Double) { _stdDeviationFactor.value = value.coerceIn(0.25, 4.0) updateRenderer() }
/** Construct and apply a StretchRenderer to the raster layer using current UI parameters. */ private fun updateRenderer() { val parameters: StretchParameters = when (_selectedStretchType.value) { StretchType.MinMax -> { // apply the values to the renderer MinMaxStretchParameters( minValues = listOf(_minValue.value), maxValues = listOf(_maxValue.value) ) }
StretchType.PercentClip -> { // apply the values to the renderer PercentClipStretchParameters( min = _percentMin.value, max = _percentMax.value ) }
StretchType.StandardDeviation -> { // apply the value to the renderer StandardDeviationStretchParameters( factor = _stdDeviationFactor.value ) } } rasterLayer.renderer = StretchRenderer( parameters = parameters, gammas = emptyList(), estimateStatistics = true, colorRamp = null ) }
/** Reset all parameters to their initial default values. */ fun resetAllChanges() { _selectedStretchType.value = StretchType.MinMax _minValue.value = DEFAULT_MIN _maxValue.value = DEFAULT_MAX _percentMin.value = DEFAULT_PERCENT_MIN _percentMax.value = DEFAULT_PERCENT_MAX _stdDeviationFactor.value = DEFAULT_STD_DEVIATION_FACTOR updateRenderer() }}
/** Enum representing available stretch parameter types. */enum class StretchType { MinMax, PercentClip, StandardDeviation }/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.applystretchrenderer.screens
import androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Settingsimport androidx.compose.material3.Buttonimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.Iconimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.RangeSliderimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.applystretchrenderer.components.ApplyStretchRendererViewModelimport com.esri.arcgismaps.sample.applystretchrenderer.components.StretchTypeimport com.esri.arcgismaps.sample.sampleslib.components.BottomSheetimport com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBoximport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the sample app. */@Composablefun ApplyStretchRendererScreen(sampleName: String) { val mapViewModel: ApplyStretchRendererViewModel = viewModel()
// UI state val selectedStretchType by mapViewModel.selectedStretchType.collectAsStateWithLifecycle() val minValue by mapViewModel.minValue.collectAsStateWithLifecycle() val maxValue by mapViewModel.maxValue.collectAsStateWithLifecycle() val percentMin by mapViewModel.percentMin.collectAsStateWithLifecycle() val percentMax by mapViewModel.percentMax.collectAsStateWithLifecycle() val stdDevFactor by mapViewModel.stdDeviationFactor.collectAsStateWithLifecycle()
var isBottomSheetVisible by remember { mutableStateOf(false) }
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { padding -> MapView( modifier = Modifier .fillMaxSize() .padding(padding), arcGISMap = mapViewModel.arcGISMap, mapViewProxy = mapViewModel.mapViewProxy, onDown = { isBottomSheetVisible = false }, )
BottomSheet( isVisible = isBottomSheetVisible, sheetTitle = "Stretch Renderer Settings", onDismissRequest = { isBottomSheetVisible = false } ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "Choose stretch type and configure parameters.", style = MaterialTheme.typography.labelLarge )
// Stretch type dropdown DropDownMenuBox( textFieldValue = when (selectedStretchType) { StretchType.MinMax -> mapViewModel.stretchTypeOptions[0] StretchType.PercentClip -> mapViewModel.stretchTypeOptions[1] StretchType.StandardDeviation -> mapViewModel.stretchTypeOptions[2] }, textFieldLabel = "Stretch Type", dropDownItemList = mapViewModel.stretchTypeOptions, onIndexSelected = mapViewModel::updateStretchTypeByIndex )
when (selectedStretchType) { StretchType.MinMax -> { MinMaxSettings( minValue = minValue, maxValue = maxValue, onMinValueChange = mapViewModel::updateMinValue, onMaxValueChange = mapViewModel::updateMaxValue ) }
StretchType.PercentClip -> { PercentClipSettings( percentMin = percentMin, percentMax = percentMax, onPercentMinChange = mapViewModel::updatePercentMin, onPercentMaxChange = mapViewModel::updatePercentMax ) }
StretchType.StandardDeviation -> { StdDevSettings( stdDevFactor = stdDevFactor, onStdDevFactorChange = mapViewModel::updateStdDeviationFactor ) } } Button( onClick = { mapViewModel.resetAllChanges() isBottomSheetVisible = false } ) { Text("Reset all changes") } } }
// Error dialog mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } }, floatingActionButton = { if (!isBottomSheetVisible) { FloatingActionButton( modifier = Modifier.padding(bottom = 36.dp, end = 12.dp), onClick = { isBottomSheetVisible = true } ) { Icon( Icons.Filled.Settings, contentDescription = "Show Renderer Settings" ) } } } )}
// UI for Min-Max stretch parameters@Composablefun MinMaxSettings( minValue: Double, maxValue: Double, onMinValueChange: (Double) -> Unit, onMaxValueChange: (Double) -> Unit, valueRange: ClosedFloatingPointRange<Float> = 0f..255f) { var range by remember { mutableStateOf(minValue.toFloat()..maxValue.toFloat()) }
Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Min-Max Parameters", style = MaterialTheme.typography.titleMedium)
Text(text = "Min Value: ${range.start.toInt()} Max Value: ${range.endInclusive.toInt()}")
RangeSlider( value = range, onValueChange = { newRange -> if (newRange.start < newRange.endInclusive) { range = newRange onMinValueChange(newRange.start.toDouble()) onMaxValueChange(newRange.endInclusive.toDouble()) }
}, valueRange = valueRange, steps = 254 // steps between 0 and 255 )
Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(text = "${valueRange.start.toInt()}") Text(text = "${valueRange.endInclusive.toInt()}") } }}
// UI for Percent Clip stretch parameters@Composablefun PercentClipSettings( percentMin: Double, percentMax: Double, onPercentMinChange: (Double) -> Unit, onPercentMaxChange: (Double) -> Unit, valueRange: ClosedFloatingPointRange<Float> = 0f..100f) { var range by remember { mutableStateOf(percentMin.toFloat()..percentMax.toFloat()) } Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Percent Clip Parameters", style = MaterialTheme.typography.titleMedium)
// Percent min slider (0 .. percentMax) Text(text = " Min %: ${range.start.toInt()} Max %: ${range.endInclusive.toInt()}") RangeSlider( value = range, onValueChange = { newRange -> // Ensure min is always less than max if (newRange.start < newRange.endInclusive) { range = newRange onPercentMinChange(newRange.start.toDouble()) onPercentMaxChange(newRange.endInclusive.toDouble()) } }, valueRange = valueRange, steps = 99 // steps between 0 and 100 ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(text = "${valueRange.start.toInt()}") Text(text = "${valueRange.endInclusive.toInt()}") } }}
// UI for Standard Deviation stretch parameters@Composablefun StdDevSettings( stdDevFactor: Double, onStdDevFactorChange: (Double) -> Unit) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Standard Deviation Parameters", style = MaterialTheme.typography.titleMedium)
// Factor slider (0.25 .. 4.0) Text(text = "Factor: %.2f".format(stdDevFactor)) Slider( value = stdDevFactor.toFloat(), onValueChange = { value -> onStdDevFactorChange(value.toDouble()) }, valueRange = 0.25f..4.0f ) }}