QGIS API Documentation 4.0.0-Norrköping (1ddcee3d0e4)
Loading...
Searching...
No Matches
qgselevationmap.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgselevationmap.cpp
3 --------------------------------------
4 Date : August 2022
5 Copyright : (C) 2022 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgselevationmap.h"
17
18#include <algorithm>
19#include <cmath>
20#include <memory>
21
22#include "qgsrasterblock.h"
23
24#include <QPainter>
25#include <QString>
26
27using namespace Qt::StringLiterals;
28
29static const int ELEVATION_OFFSET = 7900;
30static const int ELEVATION_SCALE = 1000;
31
32
33QgsElevationMap::QgsElevationMap( const QSize &size, float devicePixelRatio )
34 : mElevationImage( size, QImage::Format_ARGB32 )
35{
36 mElevationImage.fill( 0 );
37 mElevationImage.setDevicePixelRatio( devicePixelRatio );
38}
39
41 : mElevationImage( image )
42{}
43
45 : mElevationImage( other.mElevationImage )
46{
47 mPainter.reset();
48}
49
51{
52 double zScaled = ( z + ELEVATION_OFFSET ) * ELEVATION_SCALE;
53 unsigned int zInt = static_cast<unsigned int>( std::clamp( zScaled, 0., 16777215. ) ); // make sure to fit into 3 bytes
54 return QRgb( zInt | ( static_cast< unsigned int >( 0xff ) << 24 ) );
55}
56
58{
59 unsigned int zScaled = colorRaw & 0xffffff;
60 return ( ( float ) zScaled ) / ELEVATION_SCALE - ELEVATION_OFFSET;
61}
62
63std::unique_ptr<QgsElevationMap> QgsElevationMap::fromRasterBlock( QgsRasterBlock *block )
64{
65 auto elevMap = std::make_unique<QgsElevationMap>( QSize( block->width(), block->height() ) );
66 QRgb *dataPtr = reinterpret_cast<QRgb *>( elevMap->mElevationImage.bits() );
67 for ( int row = 0; row < block->height(); ++row )
68 {
69 for ( int col = 0; col < block->width(); ++col )
70 {
71 bool isNoData;
72 double value = block->valueAndNoData( row, col, isNoData );
73 if ( !isNoData )
74 *dataPtr = encodeElevation( static_cast<float>( value ) );
75 ++dataPtr;
76 }
77 }
78 return elevMap;
79}
80
82{
83 if ( &other == this )
84 return *this;
85
86 mPainter.reset();
87 mElevationImage = other.mElevationImage;
88 return *this;
89}
90
91void QgsElevationMap::applyEyeDomeLighting( QImage &img, int distance, float strength, float rendererScale ) const
92{
93 const int imgWidth = img.width(), imgHeight = img.height();
94 QRgb *imgPtr = reinterpret_cast<QRgb *>( img.bits() );
95 const QRgb *elevPtr = reinterpret_cast<const QRgb *>( mElevationImage.constBits() );
96
97 const int neighbours[] = { -1, 0, 1, 0, 0, -1, 0, 1 };
98 for ( int i = 0; i < imgWidth; ++i )
99 {
100 for ( int j = 0; j < imgHeight; ++j )
101 {
102 qgssize index = j * static_cast<qgssize>( imgWidth ) + i;
103 float factor = 0.0f;
104 float centerDepth = decodeElevation( elevPtr[index] );
105 if ( isNoData( elevPtr[index] ) )
106 continue;
107 for ( qgssize k = 0; k < 4; ++k )
108 {
109 float borderFactor = 1.0f;
110 int iNeighbour = std::clamp( i + distance * neighbours[2 * k], 0, imgWidth - 1 );
111 int jNeighbour = std::clamp( j + distance * neighbours[2 * k + 1], 0, imgHeight - 1 );
112 qgssize neighbourIndex = jNeighbour * static_cast<qgssize>( imgWidth ) + iNeighbour;
113
114 // neighbour is no data, we try to reach one pixel closer that is not no data
115 // and calculate a factor to take account of the reduction of the distance
116 if ( isNoData( elevPtr[neighbourIndex] ) )
117 {
118 if ( neighbours[2 * k] != 0 )
119 {
120 int corrI = iNeighbour;
121 int inc = neighbours[2 * k];
122 while ( corrI != i && isNoData( elevPtr[neighbourIndex] ) )
123 {
124 corrI -= inc;
125 neighbourIndex = jNeighbour * static_cast<qgssize>( imgWidth ) + corrI;
126 }
127 if ( corrI == i )
128 continue;
129
130 borderFactor = static_cast<float>( distance ) / static_cast<float>( std::abs( i - corrI ) );
131 }
132
133 if ( neighbours[2 * k + 1] != 0 )
134 {
135 int corrJ = jNeighbour;
136 int inc = neighbours[2 * k + 1];
137 while ( corrJ != j && isNoData( elevPtr[neighbourIndex] ) )
138 {
139 corrJ -= inc;
140 neighbourIndex = corrJ * static_cast<qgssize>( imgWidth ) + iNeighbour;
141 }
142 if ( corrJ == j )
143 continue;
144
145 borderFactor = static_cast<float>( distance ) / static_cast<float>( std::abs( j - corrJ ) );
146 }
147 }
148
149 // neighbour is outside the extent (right or left), we take the pixel on border
150 // and calculate a factor to take account of the reduction of the distance
151 if ( neighbours[2 * k] != 0 && std::abs( iNeighbour - i ) < distance )
152 {
153 if ( i > iNeighbour )
154 borderFactor = static_cast<float>( distance ) / static_cast<float>( i );
155 else if ( i < iNeighbour )
156 borderFactor = static_cast<float>( distance ) / static_cast<float>( ( imgWidth - i - 1 ) );
157 }
158 if ( neighbours[2 * k + 1] != 0 && std::abs( jNeighbour - j ) < distance )
159 {
160 if ( j > jNeighbour )
161 borderFactor = static_cast<float>( distance ) / static_cast<float>( j );
162 else if ( j < jNeighbour )
163 borderFactor = static_cast<float>( distance ) / static_cast<float>( imgHeight - j - 1 );
164 }
165
166 float neighbourDepth = decodeElevation( elevPtr[neighbourIndex] );
167 factor += std::max<float>( 0, -( centerDepth - neighbourDepth ) * borderFactor );
168 }
169 float shade = expf( -factor * strength / rendererScale );
170
171 QRgb c = imgPtr[index];
172 imgPtr[index]
173 = qRgba( static_cast<int>( static_cast<float>( qRed( c ) ) * shade ), static_cast<int>( static_cast<float>( qGreen( c ) ) * shade ), static_cast<int>( static_cast<float>( qBlue( c ) ) * shade ), qAlpha( c ) );
174 }
175 }
176}
177
178void QgsElevationMap::applyHillshading( QImage &img, bool multiDirectional, double altitude, double azimuth, double zFactor, double cellSizeX, double cellSizeY ) const
179{
180 // algs from src/raster/qgshillshaderenderer.cpp
181 double altRad = altitude * M_PI / 180.0;
182 double cos_altRadian = std::cos( altRad );
183 double sin_altRadian = std::sin( altRad );
184 double cos_alt_mul_z = cos_altRadian * zFactor;
185 double azimuthRad = -1 * azimuth * M_PI / 180.0;
186 double cos_az_mul_cos_alt_mul_z = std::cos( azimuthRad ) * cos_alt_mul_z;
187 double sin_az_mul_cos_alt_mul_z = std::sin( azimuthRad ) * cos_alt_mul_z;
188
189 // From qgshillshaderenderer.cpp, -32.87001872802012 comes from ( 127.0 * std::cos(225.0 * M_PI / 180.0))
190 // but this expression actually equals -89.8025612106916. To be consistent, we keep -32.87001872802012
191 // and divide it by 254 because, here, values are between 0.0 and 1.0, not between 0 and 255
192 double cos225_az_mul_cos_alt_mul_z_mul_0_5 = -32.87001872802012 * cos_alt_mul_z / 255;
193 double square_z = zFactor * zFactor;
194
195 QRgb *imgPtr = reinterpret_cast<QRgb *>( img.bits() );
196 const QRgb *elevPtr = reinterpret_cast<const QRgb *>( mElevationImage.constBits() );
197
198 const int imgWidth = img.width(), imgHeight = img.height();
199
200 auto colRowToIndex = [&]( int c, int r ) -> qgssize { return static_cast<qgssize>( r ) * imgWidth + c; };
201
202 float noData = noDataValue();
203
204 // Elevation value matrix
205 // 0 1 2 11 12 13
206 // 3 4 5 21 22 23
207 // 6 7 8 31 32 33
208 float pixelValues[9];
209
210 for ( int rowC = 0; rowC < imgHeight; ++rowC )
211 {
212 int rowU = std::max( 0, rowC - 1 );
213 int rowD = std::min( imgHeight - 1, rowC + 1 );
214
215 pixelValues[1] = decodeElevation( elevPtr[colRowToIndex( 0, rowU )] );
216 pixelValues[4] = decodeElevation( elevPtr[colRowToIndex( 0, rowC )] );
217 pixelValues[7] = decodeElevation( elevPtr[colRowToIndex( 0, rowD )] );
218
219 pixelValues[2] = decodeElevation( elevPtr[colRowToIndex( 1, rowU )] );
220 pixelValues[5] = decodeElevation( elevPtr[colRowToIndex( 1, rowC )] );
221 pixelValues[8] = decodeElevation( elevPtr[colRowToIndex( 1, rowD )] );
222
223
224 for ( int colC = 0; colC < imgWidth; ++colC )
225 {
226 qgssize centerIndex = colRowToIndex( colC, rowC );
227 int colR = std::min( imgWidth - 1, colC + 1 );
228
229 pixelValues[0] = pixelValues[1];
230 pixelValues[3] = pixelValues[4];
231 pixelValues[6] = pixelValues[7];
232
233 pixelValues[1] = pixelValues[2];
234 pixelValues[4] = pixelValues[5];
235 pixelValues[7] = pixelValues[8];
236
237 pixelValues[2] = decodeElevation( elevPtr[colRowToIndex( colR, rowU )] );
238 pixelValues[5] = decodeElevation( elevPtr[colRowToIndex( colR, rowC )] );
239 pixelValues[8] = decodeElevation( elevPtr[colRowToIndex( colR, rowD )] );
240
241 if ( elevPtr[centerIndex] != 0 )
242 {
243 // This is center cell. Use this in place of nodata neighbors
244 const float x22 = pixelValues[4];
245 if ( x22 == noData )
246 continue;
247
248 const float x11 = ( pixelValues[0] == noData ) ? x22 : pixelValues[0];
249 const float x21 = ( pixelValues[3] == noData ) ? x22 : pixelValues[3];
250 const float x31 = ( pixelValues[6] == noData ) ? x22 : pixelValues[6];
251
252 const float x12 = ( pixelValues[1] == noData ) ? x22 : pixelValues[1];
253 const float x32 = ( pixelValues[7] == noData ) ? x22 : pixelValues[7];
254
255 const float x13 = ( pixelValues[2] == noData ) ? x22 : pixelValues[2];
256 const float x23 = ( pixelValues[5] == noData ) ? x22 : pixelValues[5];
257 const float x33 = ( pixelValues[8] == noData ) ? x22 : pixelValues[8];
258
259 const double derX = static_cast<double>( ( x13 + x23 + x23 + x33 ) - ( x11 + x21 + x21 + x31 ) ) / ( 8 * cellSizeX );
260 const double derY = static_cast<double>( ( x31 + x32 + x32 + x33 ) - ( x11 + x12 + x12 + x13 ) ) / ( 8 * -cellSizeY );
261
262 double shade = 0;
263 if ( !multiDirectional )
264 {
265 // Standard single direction hillshade
266 shade = std::clamp( ( sin_altRadian - ( derY * cos_az_mul_cos_alt_mul_z - derX * sin_az_mul_cos_alt_mul_z ) ) / std::sqrt( 1 + square_z * ( derX * derX + derY * derY ) ), 0.0, 1.0 );
267 }
268 else
269 {
270 // Weighted multi direction as in http://pubs.usgs.gov/of/1992/of92-422/of92-422.pdf
271 // Fast formula from GDAL DEM
272 const double xx = derX * derX;
273 const double yy = derY * derY;
274 const double xx_plus_yy = xx + yy;
275 // Flat?
276 if ( xx_plus_yy == 0.0 )
277 {
278 shade = std::clamp( sin_altRadian, 0.0, 1.0 );
279 }
280 else
281 {
282 // ... then the shade value from different azimuth
283 double val225_mul_0_5 = sin_altRadian * 0.5 + ( derX - derY ) * cos225_az_mul_cos_alt_mul_z_mul_0_5;
284 val225_mul_0_5 = ( val225_mul_0_5 <= 0.0 ) ? 0.0 : val225_mul_0_5;
285 double val270_mul_0_5 = sin_altRadian * 0.5 - derX * cos_alt_mul_z * 0.5;
286 val270_mul_0_5 = ( val270_mul_0_5 <= 0.0 ) ? 0.0 : val270_mul_0_5;
287 double val315_mul_0_5 = sin_altRadian * 0.5 + ( derX + derY ) * cos225_az_mul_cos_alt_mul_z_mul_0_5;
288 val315_mul_0_5 = ( val315_mul_0_5 <= 0.0 ) ? 0.0 : val315_mul_0_5;
289 double val360_mul_0_5 = sin_altRadian * 0.5 - derY * cos_alt_mul_z * 0.5;
290 val360_mul_0_5 = ( val360_mul_0_5 <= 0.0 ) ? 0.0 : val360_mul_0_5;
291
292 // ... then the weighted shading
293 const double weight_225 = 0.5 * xx_plus_yy - derX * derY;
294 const double weight_270 = xx;
295 const double weight_315 = xx_plus_yy - weight_225;
296 const double weight_360 = yy;
297 const double cang_mul_127 = ( ( weight_225 * val225_mul_0_5 + weight_270 * val270_mul_0_5 + weight_315 * val315_mul_0_5 + weight_360 * val360_mul_0_5 ) / xx_plus_yy )
298 / ( 1 + square_z * xx_plus_yy );
299
300 shade = std::clamp( cang_mul_127, 0.0, 1.0 );
301 }
302 }
303
304 QRgb c = imgPtr[centerIndex];
305 imgPtr[centerIndex]
306 = qRgba( static_cast<int>( static_cast<float>( qRed( c ) ) * shade ), static_cast<int>( static_cast<float>( qGreen( c ) ) * shade ), static_cast<int>( static_cast<float>( qBlue( c ) ) * shade ), qAlpha( c ) );
307 }
308 }
309 }
310}
311
313{
314 if ( !mPainter )
315 {
316 mPainter = std::make_unique<QPainter>();
317 mPainter->begin( &mElevationImage );
318 }
319 return mPainter.get();
320}
321
323{
324 if ( otherElevationMap.mElevationImage.size() != mElevationImage.size() )
325 {
326 QgsDebugMsgLevel( u"Elevation map with different sizes can not be combined"_s, 4 );
327 return;
328 }
329
330 QRgb *elevPtr = reinterpret_cast<QRgb *>( mElevationImage.bits() );
331 const QRgb *otherElevPtr = reinterpret_cast<const QRgb *>( otherElevationMap.mElevationImage.constBits() );
332
333 int width = mElevationImage.width();
334 int height = mElevationImage.height();
335
336 for ( int row = 0; row < height; ++row )
337 {
338 for ( int col = 0; col < width; ++col )
339 {
340 qgssize index = col + static_cast<qgssize>( row ) * width;
341 if ( !isNoData( otherElevPtr[index] ) )
342 {
343 switch ( method )
344 {
346 {
347 double elev = decodeElevation( elevPtr[index] );
348 double otherElev = decodeElevation( otherElevPtr[index] );
349 if ( otherElev > elev )
350 elevPtr[index] = otherElevPtr[index];
351 }
352 break;
354 elevPtr[index] = otherElevPtr[index];
355 break;
356 }
357 }
358 }
359 }
360}
361
362
364{
365 return !mElevationImage.isNull();
366}
367
368void QgsElevationMap::fillWithRasterBlock( QgsRasterBlock *block, int top, int left, double zScale, double offset )
369{
370 QRgb *dataPtr = reinterpret_cast<QRgb *>( mElevationImage.bits() );
371
372 int widthMap = mElevationImage.width();
373 int heightMap = mElevationImage.height();
374 int widthBlock = block->width();
375 int combinedHeight = std::min( mElevationImage.height(), block->height() + top );
376 int combinedWidth = std::min( widthMap, widthBlock + left );
377 for ( int row = std::max( 0, top ); row < combinedHeight; ++row )
378 {
379 if ( row >= heightMap )
380 continue;
381
382 for ( int col = std::max( 0, left ); col < combinedWidth; ++col )
383 {
384 if ( col >= widthMap )
385 continue;
386
387 bool isNoData = true;
388 double value = block->valueAndNoData( ( row - top ), ( col - left ), isNoData );
389 qgssize index = col + static_cast<qgssize>( row ) * widthMap;
390 if ( isNoData )
391 dataPtr[index] = 0;
392 else
393 dataPtr[index] = encodeElevation( static_cast<float>( value * zScale + offset ) );
394 }
395 }
396}
ElevationMapCombineMethod
Methods used to select the elevation when two elevation maps are combined.
Definition qgis.h:5072
@ NewerElevation
Keep the new elevation regardless of its value if it is not null.
Definition qgis.h:5074
@ HighestElevation
Keep the highest elevation if it is not null.
Definition qgis.h:5073
void combine(const QgsElevationMap &otherElevationMap, Qgis::ElevationMapCombineMethod method)
Combines this elevation map with otherElevationMap.
static QRgb encodeElevation(float z)
Converts elevation value to an actual color.
QgsElevationMap()=default
void applyEyeDomeLighting(QImage &image, int distance, float strength, float rendererScale) const
Applies eye dome lighting effect to the given image.
static std::unique_ptr< QgsElevationMap > fromRasterBlock(QgsRasterBlock *block)
Creates an elevation map based on data from the given raster block.
QPainter * painter() const
Returns painter to the underlying QImage with elevations.
float noDataValue() const
Returns the no data value for the elevation map.
bool isNoData(QRgb colorRaw) const
Returns whether the encoded value is a no data value.
bool isValid() const
Returns whether the elevation map is valid.
static float decodeElevation(QRgb colorRaw)
Converts a color back to elevation value.
void fillWithRasterBlock(QgsRasterBlock *block, int top, int left, double zScale=1.0, double offset=0.0)
Fills the elevation map with values contains in a raster block starting from position defined by top ...
void applyHillshading(QImage &image, bool multiDirectional, double altitude, double azimuth, double zFactor, double cellSizeX, double cellSizeY) const
Applies hill shading effect to the given image.
QgsElevationMap & operator=(const QgsElevationMap &other)
Raster data container.
int height() const
Returns the height (number of rows) of the raster block.
double valueAndNoData(int row, int column, bool &isNoData) const
Reads a single value from the pixel at row and column, if type of block is numeric.
int width() const
Returns the width (number of columns) of the raster block.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
unsigned long long qgssize
Qgssize is used instead of size_t, because size_t is stdlib type, unknown by SIP, and it would be har...
Definition qgis.h:7485
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63