Application Utility – Reading geospatial raster files using GDAL OpenCV v4.8.0

Previous tutorial: Adding a track bar to our application

Next tutorial: Video input using OpenCV and similarity measurement

Original author Marvin Smith
Compatibility OpenCV >= 3.0

Geospatial raster data is a product heavily used in geographic information systems and photogrammetry. Raster data can often represent images and digital elevation models (DEMs). The standard library for loading GIS images is the Geographic Data Abstraction Library (GDAL). In this example, we will demonstrate techniques for loading a GIS raster format using OpenCV native functions. Additionally, we will provide examples of how OpenCV can use this data for new and interesting purposes.

Goals

The main goal of this tutorial is to

  • How to load satellite images using OpenCV imread.
  • How to use OpenCV imread – Load SRTM digital elevation model
  • Given an image and the angular coordinates of a DEM, associate the elevation data with the image to find the elevation of each pixel.
  • Shows a simple and easy to use terrain heat map example.
  • Demonstrates the basic use of DEM data with orthophotos.

To achieve these goals, the following code takes as input a digital elevation model and a GeoTiff image of San Francisco. After processing the image and DEM data, a topographic heat map of the image is generated and identifies areas of the city that would be affected when the bay water level rises by 10, 50 and 100 meters.

Code

/*
 * gdal_image.cpp -- Load GIS data into an OpenCV container using the geospatial data abstraction library
*/
// OpenCV header file
#include "opencv2/core.hpp
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp" */ // C++ standard library
// C++ standard library
#include <cmath
#include <iostream
#include <stdexcept
#include <vector
using namespace std;
//define corner points
// Note that the GDAL library can determine this natively
cv::Point2d tl(-122.441017, 37.815664);
cv::Point2d tr(-122.370919, 37.815311);
cv::Point2d bl(-122.441533, 37.747167);
cv::Point2d br(-122.3715, 37.746814);
// Determine the presentation angle
cv::Point2d dem_bl( -122.0, 38);
cv::Point2d dem_tr( -123.0, 37);
//Heat map color range
std::vector<std::pair<cv::Vec3b,double> > color_range;
// List of all function prototypes
cv::Point2d lerp( const cv::Point2d & amp;, const cv::Point2d & amp;, const double & amp; );
cv::Vec3b get_dem_color( const double & amp; );
cv::Point2d world2dem( const cv::Point2d & amp;, const cv::Size & amp;);
cv::Point2d pixel2world( const int & amp;, const int & amp;, const cv::Size & amp; );
void add_color( cv::Vec3b & amp; pix, const uchar & amp; b, const uchar & amp; g, const uchar & amp; r );
/*
 * Linear interpolation
 * p1 - point 1
 * p2 - point 2
 *t - Ratio from point 1 to point 2
*/
cv::Point2d lerp( cv::Point2d const & amp; p1, cv::Point2d const & amp; p2, const double & amp; t ){<!-- -->
 return cv::Point2d( ((1-t)*p1.x) + (t*p2.x)、
 ((1-t)*p1.y) + (t*p2.y));
}
/*
 * Interpolation color
*/
template <typename DATATYPE, int N>
cv::Vec<DATATYPE,N> lerp( cv::Vec<DATATYPE,N> const & minColor、
 cv::Vec<DATATYPE,N> const & maxColor、
 double const & amp; t ){<!-- -->
 cv::Vec<DATATYPE,N> output;
 for( int i=0; i<N; i + + ){<!-- -->
 output[i] = (uchar)(((1-t)*minColor[i]) + (t * maxColor[i]));
 }
 return output;
}
/*
 * Calculate demo color
*/
cv::Vec3b get_dem_color( const double & amp; elevation ){<!-- -->
 // If the altitude is lower than the minimum value, return the minimum value
 if( elevation < color_range[0].second ){<!-- -->
 return color_range[0].first;
 }
 // If the altitude is higher than the maximum value, return the maximum value
 if( elevation > color_range.back().second ){<!-- -->
 return color_range.back().first;
 }
 // Otherwise, find a suitable starting index
 int idx=0;
 double t = 0;
 for( int x=0; x<(int)(color_range.size()-1); x + + ){<!-- -->
 // If the current height is lower than the next item, use the current
 // Two colors as our range
 if( elevation < color_range[x + 1].second ){<!-- -->
 idx=x;
 t = (color_range[x + 1].second - elevation)/ (color_range[x + 1].second - elevation)/ (color_range[x + 1].second)
 (color_range[x + 1].second - color_range[x].second);
 break;
 }
 }
 // interpolate color
 return lerp(color_range[idx].first, color_range[idx + 1].first, t);
}
/*
 * Given pixel coordinates and the size of the input image, calculate the position of the pixel on the DEM image.
 * Pixel location on the DEM image.
*/
cv::Point2d world2dem( cv::Point2d const & amp; coordinate, const cv::Size & amp; dem_size ){<!-- --> // Associate it with the DEM point.
 // Associate it with the DEM point
 // Assume that dem data is orthogonally corrected
 double demRatioX = ((dem_tr.x - coordinate.x)/(dem_tr.x - dem_bl.x));
 double demRatioY = 1-((dem_tr.y-coordinate.y)/(dem_tr.y-dem_bl.y));
 cv::Point2d output;
 output.x = demRatioX * dem_size.width;
 output.y = demRatioY * dem_size.height;
 return output;
}
/*
 * Convert pixel coordinates to world coordinates
*/

cv::Point2d pixel2world( const int & amp; x, const int & amp; y, const cv::Size & amp; size ) {<!-- --> // Calculate the ratio of pixel position to size.
 // Calculate the ratio of pixel position to its size
 double rx = (double)x / size.width;
 double ry = (double)y / size.height;
 // Calculate LERP for each coordinate
 cv::Point2d rightSide = lerp(tr, br, ry);
 cv::Point2d leftSide = lerp(tl, bl, ry);
 // Calculate the actual latitude/longitude coordinates of the interpolated coordinates
 return lerp(leftSide, rightSide, rx);
}
/*
 * Add color to specific pixel color value
*/
void add_color( cv::Vec3b & amp; pix, const uchar & amp; b, const uchar & amp; g, const uchar & amp; r ){<!-- -->
 if( pix[0] + b < 255 & amp; & amp; pix[0] + b >= 0 ) {<!-- --> pix[0] + = b; }
 if( pix[1] + g < 255 & amp; & amp; pix[1] + g >= 0 ){<!-- --> pix[1] + = g; }
 if( pix[2] + r < 255 & amp; & amp; pix[2] + r >= 0 ){<!-- --> pix[2] + = r; }
}
/*
 * Main function
*/
int main( int argc, char* argv[] ){<!-- -->
 /*
 * Check input parameters
 */
 if( argc < 3 ){<!-- -->
 cout << "usage: " << argv[0] << " <image_name> <dem_model_name>" << endl;
 return -1;
 }
 // Load the image (note that we have no projection information. You will
 // Need to load it yourself or use the full GDAL driver. These values are predefined
 // at the top of this file
 cv::Mat image = cv::imread(argv[1], cv::IMREAD_LOAD_GDAL | cv::IMREAD_COLOR);
 //Load dem model
 cv::Mat dem = cv::imread(argv[2], cv::IMREAD_LOAD_GDAL | cv::IMREAD_ANYDEPTH );
 //Create output product
 cv::Mat output_dem( image.size(), CV_8UC3 );
 cv::Mat output_dem_flood( image.size(), CV_8UC3 );
 // For sanity's sake, make sure GDAL loads this as a signed short
 if( dem.type() != CV_16SC1 ){<!-- --> throw std::runtime_error("DEM image type must be CV_16SC1"); } }
 // Define the color range to create the output DEM heatmap
 // Pair format ( Color, elevation ); Push from low to high
 // NOTE: This is great for config files, but here is for a working demo.
 color_range.push_back( std::pair<cv::Vec3b,double>(cv::Vec3b( 188, 154, 46), -1));
 color_range.push_back( std::pair<cv::Vec3b,double>(cv::Vec3b( 110, 220, 110), 0.25));
 color_range.push_back( std::pair<cv::Vec3b,double>(cv::Vec3b( 150, 250, 230), 20);
 color_range.push_back( std::pair<cv::Vec3b,double>(cv::Vec3b( 160, 220, 200), 75));
 color_range.push_back( std::pair<cv::Vec3b,double>(cv::Vec3b( 220, 190, 170), 100));
 color_range.push_back( std::pair<cv::Vec3b,double>(cv::Vec3b( 250, 180, 140), 200));
 //Define the minimum elevation
 double minElevation = -10;
 // Traverse each pixel in the image and calculate the presentation point
 for( int y=0; y<image.rows; y + + ){<!-- -->
 for( int x=0; x<image.cols; x + + ){<!-- -->
 //Convert pixel coordinates to latitude/latitude coordinates
 cv::Point2d coordinate = pixel2world(x, y, image.size());
 // Calculate dem image pixel coordinates based on latitude/longitudinal coordinates
 cv::Point2d dem_coordinate = world2dem( coordinate, dem.size() );
 //Extract altitude
 doubledz;
 if( dem_coordinate.x >= 0 & amp; & amp; dem_coordinate.y >= 0 & amp; & amp;
 dem_coordinate.x < dem.cols & amp; & amp; dem_coordinate.y < dem.rows ){<!-- -->
 dz = dem.at<short>(dem_coordinate);
 }else{<!-- -->
 dz = minElevation;
 }
 //Write pixel values to file
 output_dem_flood.at<cv::Vec3b>(y,x) = image.at<cv::Vec3b>(y,x);
 // Calculate the color of the heatmap output
 cv::Vec3b actualColor = get_dem_color(dz);
 output_dem.at<cv::Vec3b>(y,x) = actualColor;
 // Show the impact of 10 meters of sea level rise
 if( dz < 10 ){<!-- -->
 add_color( output_dem_flood.at<cv::Vec3b>(y,x), 90, 0, 0);
 }
 // Show the impact of 50 meters of sea level rise
 else if( dz < 50 ){<!-- -->
 add_color( output_dem_flood.at<cv::Vec3b>(y,x), 0, 90, 0);
 }
 // Show the impact of 100 meters of sea level rise
 else if( dz < 100 ){<!-- -->
 add_color( output_dem_flood.at<cv::Vec3b>(y,x), 0, 0, 90);
 }
 }}
 // print heat map
 cv::imwrite( "heat-map.jpg" , output_dem );
 //Print water immersion effect image
 cv::imwrite( "flooded.jpg", output_dem_flood);
 return 0;
}

How to read raster data using GDAL

This demonstration uses the default OpenCV imread function. The main difference is that in order to force GDAL to load an image, the appropriate flags must be used.

 cv::Mat image = cv::imread(argv[1], cv::IMREAD_LOAD_GDAL | cv::IMREAD_COLOR);

When loading a digital elevation model, the actual value of each pixel is critical and cannot be scaled or truncated. For example, in image data, a pixel represented as a binary number with a value of 1 has the same appearance as a pixel represented as an unsigned character with a value of 255. For terrain data, the pixel value represents the altitude in meters. To ensure that OpenCV preserves the original values, use the GDAL flag and the ANYDEPTH flag in imread.

 //Load dem model
 cv::Mat dem = cv::imread(argv[2], cv::IMREAD_LOAD_GDAL | cv::IMREAD_ANYDEPTH );

If the type of the DEM model to be loaded is known in advance, it may be safer to test Mat::type() or Mat::depth() using assertions or other mechanisms. NASA or DOD specification documents can provide input types for various elevation models. The main types SRTM and DTED are signed dashes.

Comments

Latitude/longitude (geographical) coordinates should generally be avoided

Geographic coordinate systems are a spherical coordinate system, which means it is technically incorrect to use them in Cartesian mathematics. This demonstration uses these coordinate systems for readability and to be sufficiently accurate. A better coordinate system is the Universal Transverse Mercator coordinate system.

Find corner coordinates

An easy way to find the corner coordinates of an image is to use the command line tool gdalinfo. For orthorectified images that include projection information, use USGS EarthExplorer.

\f$> gdalinfo N37W123.hgt
 Driver: SRTMHGT/SRTMHGT File Format
 Files: N37W123.hgt
 Size is 3601, 3601
 Coordinate System is:
 GEOGCS["WGS 84",
 DATUM["WGS_1984",
 ... more output ...
 Corner Coordinates:
 Upper Left (-123.0001389, 38.0001389) (123d 0' 0.50"W, 38d 0' 0.50"N)
 Lower Left (-123.0001389, 36.9998611) (123d 0' 0.50"W, 36d59'59.50"N)
 Upper Right (-121.9998611, 38.0001389) (121d59'59.50"W, 38d 0' 0.50"N)
 Lower Right (-121.9998611, 36.9998611) (121d59'59.50"W, 36d59'59.50"N)
 Center (-122.5000000, 37.5000000) (122d30' 0.00"W, 37d30' 0.00"N)
 ... more output ...

Results

The following is the output of the program. Use the first image as input. For the DEM model, download the SRTM file located at USGS here. http://dds.cr.usgs.gov/srtm/version2_1/SRTM1/Region_04/N37W123.hgt.zip


Input image


Heat Map


Heat map overlay