ryujin 2.1.1 revision ee5cbcbf2346c1299c942d0e1f13b46449973c18
Loading...
Searching...
No Matches
geotiff_reader.h
Go to the documentation of this file.
1//
2// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
3// Copyright (C) 2024 - 2025 by the ryujin authors
4//
5
6#pragma once
7
8#include <compile_time_options.h>
9
10#include "convenience_macros.h"
11#include "lazy.h"
12#include "patterns_conversion.h"
13
14#include <deal.II/base/parameter_acceptor.h>
15
16#include <numeric>
17
18#ifdef WITH_GDAL
19#include <cpl_conv.h>
20#include <gdal.h>
21#include <gdal_priv.h>
22#endif
23
24namespace ryujin
25{
34 none,
36 minimum,
38 average,
41 };
42} // namespace ryujin
43
44#ifndef DOXYGEN
50#endif
51
52namespace ryujin
53{
61 class GeoTIFFReader : dealii::ParameterAcceptor
62 {
63 public:
64 GeoTIFFReader(const std::string subsection)
65 : ParameterAcceptor(subsection)
66 {
67 filename_ = "ryujin.tif";
68 this->add_parameter("filename", filename_, "GeoTIFF: image file to load");
69
70 transformation_ = {0., 0.01, 0., 0., 0., 0.01};
71 this->add_parameter(
72 "transformation",
73 transformation_,
74 "Array \"t[]\" describing an affine transformation between image "
75 "space (indices i and j from bottom left) and real coordinates (x "
76 "and y): x = t[0] + t[1] * i + t[2] * j, and y = t[3] + t[4] * i + "
77 "t[5] * j. (This transformation sets the origin of the image space "
78 "into the bottom left corner with index i to the right and index j "
79 "up)");
80
81 transformation_allow_out_of_bounds_queries_ = false;
82 this->add_parameter(
83 "transformation allow out of bounds queries",
84 transformation_allow_out_of_bounds_queries_,
85 "GeoTIFF: allow out-of-bounds queries. When set to true, the reader "
86 "returns constant extended values for coordinates that are outside "
87 "of the image range.");
88
89 transformation_use_geotiff_ = true;
90 this->add_parameter("transformation use geotiff",
91 transformation_use_geotiff_,
92 "GeoTIFF: read in transformation from GeoTIFF for "
93 "constructing the affine transformation. If set to "
94 "false the manually specified transformation "
95 "parameters will be used instead.");
96
97 transformation_use_geotiff_origin_ = false;
98 this->add_parameter(
99 "transformation use geotiff origin",
100 transformation_use_geotiff_origin_,
101 "GeoTIFF: read in affine shift (i.e., position of "
102 "lower left corner) from GeoTIFF for constructing "
103 "the affine transformation. If set to false the origin specified "
104 "in the transformation parameter will be used instead.");
105
106 height_normalization_ = HeightNormalization::minimum;
107 this->add_parameter("height normalization",
108 height_normalization_,
109 "GeoTIFF: choose base point for height normalization "
110 "that is set to 0.: none, minimum, average, maximum");
111
112 height_scaling_ = 1.0;
113 this->add_parameter("height scaling",
114 height_scaling_,
115 "GeoTIFF: choose base point for height normalization "
116 "that is set to 0.: none, minimum, average, maximum");
117
118 const auto set_up = [&] {
119#ifdef WITH_GDAL
120 /* Initial GDAL and reset all data: */
121 GDALAllRegister();
122 driver_name_ = "";
123 driver_projection_ = "";
124 affine_transformation_ = {0, 0, 0, 0, 0, 0};
125 inverse_affine_transformation_ = {0, 0, 0, 0, 0, 0};
126 raster_offset_ = {0, 0};
127 raster_size_ = {0, 0};
128 raster_.clear();
129#endif
130 };
131
132 set_up();
133 this->parse_parameters_call_back.connect(set_up);
134 }
135
136 /*
137 * Query height information for a given position. The method is
138 * templated in dim, so that it can be called with a @p point having an
139 * arbitrary dimension. The method, however, only only uses the first
140 * two coordinates, x and y, from @p point, translates (x, y) into
141 * image coordinates (i, j) and returns an interpolated height value.
142 *
143 * @note If dim < 2, then the y coordinate is set to zero. Similarly,
144 * if dim == 0, then the tiff raster is queried at coordinate (0, 0).
145 */
146 template <int dim>
147 DEAL_II_ALWAYS_INLINE inline double
148 compute_height(const dealii::Point<dim> &point) const
149 {
150 geotiff_guard_.ensure_initialized([&]() {
151 read_in_raster();
152 return true;
153 });
154
155 double x = 0;
156 if constexpr (dim >= 1)
157 x = point[0];
158
159 double y = 0;
160 if constexpr (dim >= 2)
161 y = point[1];
162
163 const auto &[di, dj] = apply_inverse_transformation(x, y);
164
165 /* Check that we are in bounds: */
166
167 const bool in_bounds =
168 di > -1.0 && di < static_cast<double>(raster_size_[0]) + 1.0 &&
169 dj > -1.0 && dj < static_cast<double>(raster_size_[1]) + 1.0;
170
171#ifdef DEBUG_OUTPUT
172 if (!in_bounds) {
173 std::cout << std::setprecision(16);
174 std::cout << "Queried point out of bounds." << std::endl;
175 std::cout << "Point: " << point << std::endl;
176 std::cout << "Transformed coordinates: (" << di << "," << dj << ")"
177 << std::endl;
178 }
179#endif
180
181 AssertThrow(
182 transformation_allow_out_of_bounds_queries_ || in_bounds,
183 dealii::ExcMessage("Raster error: The requested point is outside "
184 "the image boundary of the geotiff file"));
185
186 /*
187 * Use a simple bilinear interpolation and ensure we never go below
188 * the minimum or above the maximum index.
189 */
190
191 const auto i_left = std::min(
192 std::max(static_cast<int>(std::floor(di)), 0), raster_size_[0]);
193 const auto i_right = std::min(
194 std::max(static_cast<int>(std::ceil(di)), 0), raster_size_[0]);
195 const auto j_left = std::min(
196 std::max(static_cast<int>(std::floor(dj)), 0), raster_size_[1]);
197 const auto j_right = std::min(
198 std::max(static_cast<int>(std::ceil(dj)), 0), raster_size_[1]);
199
200#ifdef DEBUG_OUTPUT
201 if (!in_bounds) {
202 std::cout << "index bounding box: (" << i_left << "," << j_left
203 << ") and (" << i_right << "," << j_right << ")" << std::endl;
204 }
205#endif
206
207 const double i_ratio = std::fmod(di, 1.);
208 const double j_ratio = std::fmod(dj, 1.);
209
210 const auto v_iljl = raster_[i_left + j_left * raster_size_[0]];
211 const auto v_irjl = raster_[i_right + j_left * raster_size_[0]];
212
213 const auto v_iljr = raster_[i_left + j_right * raster_size_[0]];
214 const auto v_irjr = raster_[i_right + j_right * raster_size_[0]];
215
216 const auto v_jl = v_iljl * (1. - i_ratio) + v_irjl * i_ratio;
217 const auto v_jr = v_iljr * (1. - i_ratio) + v_irjr * i_ratio;
218
219 return height_scaling_ * (v_jl * (1. - j_ratio) + v_jr * j_ratio);
220 }
221
222 /*
223 * Return the affine transformation information that is stored in the
224 * GeoTIFF image.
225 */
227
231
232 private:
233 void read_in_raster() const
234 {
235#ifdef WITH_GDAL
236 auto dataset_handle = GDALOpen(filename_.c_str(), GA_ReadOnly);
237 AssertThrow(dataset_handle,
238 dealii::ExcMessage("GDAL error: file not found"));
239
240 auto dataset = GDALDataset::FromHandle(dataset_handle);
241 Assert(dataset, dealii::ExcInternalError());
242
243 const auto driver = dataset->GetDriver();
244
245 driver_name_ = driver->GetMetadataItem(GDAL_DMD_LONGNAME);
246 if (dataset->GetProjectionRef() != nullptr)
247 driver_projection_ = dataset->GetProjectionRef();
248
249 /* For now we support one raster in the dataset: */
250
251 AssertThrow(
252 dataset->GetRasterCount() == 1,
253 dealii::ExcMessage(
254 "GDAL driver error: currently we only support one raster"));
255
256 const auto raster_band = dataset->GetRasterBand(1);
257
258 AssertThrow(dataset->GetRasterXSize() == raster_band->GetXSize() &&
259 dataset->GetRasterYSize() == raster_band->GetYSize(),
260 dealii::ExcMessage(
261 "GDAL driver error: the raster band has a different "
262 "dimension than the (global) raster dimension of the "
263 "geotiff image. This is not supported."));
264
265 /*
266 * FIXME: For now, we simply read in the entire geotiff file on
267 * each rank. In order to save memory for very large files it would
268 * be possible to create a bounding box for the all active cells of
269 * the triangulation and then only read in a small region for which
270 * we actually need data.
271 */
272
273 raster_offset_ = {0, 0};
274 raster_size_ = {dataset->GetRasterXSize(), dataset->GetRasterYSize()};
275
276 raster_.resize(raster_size_[0] * raster_size_[1]);
277 const auto error_code = raster_band->RasterIO(
278 GF_Read,
279 raster_offset_[0], /* x-offset of image region */
280 raster_offset_[1], /* y-offset of image region */
281 raster_size_[0], /* x-size of image region */
282 raster_size_[1], /* y-size of image region */
283 raster_.data(),
284 raster_size_[0], /* x-size of target buffer */
285 raster_size_[1], /* y-size of target buffer */
286 GDT_Float32,
287 0,
288 0);
289
290 AssertThrow(error_code == 0,
291 dealii::ExcMessage(
292 "GDAL driver error: error reading in geotiff file"));
293
294 /*
295 * Read in the affine transformation from the geotiff image.
296 *
297 * Note that this transformation differs from the one we use in the
298 * parameter file: GDAL uses typical image orientation: the origin
299 * of the dataset is in the "top left" corner (instead of bottom
300 * left) and the first (column) index goes to the right and the
301 * second (row) index goes down.
302 */
303
304 if (transformation_use_geotiff_) {
305 const auto success =
306 dataset->GetGeoTransform(affine_transformation_.data()) == CE_None;
307 AssertThrow(success,
308 dealii::ExcMessage("GDAL driver error: no geo transform "
309 "present in geotiff file"));
310 } else {
311 affine_transformation_ = transformation_;
312 /* Flip sign for j index (y-coordinate): */
313 affine_transformation_[2] *= -1.;
314 affine_transformation_[5] *= -1.;
315 }
316
317 /*
318 * Ensure that (i=0, j=raster_size[1]-1) corresponds to the user
319 * supplied (transformation_[0], transformation_[3]).
320 */
321 if (transformation_use_geotiff_ == false ||
322 transformation_use_geotiff_origin_ == false) {
323 const auto j_max = raster_size_[1] - 1;
324 affine_transformation_[0] =
325 transformation_[0] - j_max * affine_transformation_[2];
326 affine_transformation_[3] =
327 transformation_[3] - j_max * affine_transformation_[5];
328 }
329
330 /*
331 * Compute inverse transformation of
332 *
333 * x = t[0] + t[1] * i + t[2] * j, y = t[3] + t[4] * i + t[5] * j.
334 *
335 * namely:
336 *
337 * i = it[1] * (x - it[0]) + it[2] * (y - it[3])
338 * j = it[4] * (x - it[0]) + it[5] * (y - it[3])
339 */
340 inverse_affine_transformation_[0] = affine_transformation_[0];
341 inverse_affine_transformation_[3] = affine_transformation_[3];
342
343 const auto determinant =
344 affine_transformation_[1] * affine_transformation_[5] -
345 affine_transformation_[2] * affine_transformation_[4];
346 const auto inv = 1. / determinant;
347 inverse_affine_transformation_[1] = inv * affine_transformation_[5];
348 inverse_affine_transformation_[2] = inv * (-affine_transformation_[2]);
349 inverse_affine_transformation_[4] = inv * (-affine_transformation_[4]);
350 inverse_affine_transformation_[5] = inv * affine_transformation_[1];
351
352 GDALClose(dataset_handle);
353
354#ifdef DEBUG_OUTPUT
355 std::cout << std::setprecision(16);
356 std::cout << "GDAL: driver name = " << driver_name_;
357 std::cout << "\nGDAL: projection = " << driver_projection_;
358 std::cout << "\nGDAL: transformation =";
359 for (const auto &it : affine_transformation_)
360 std::cout << " " << it;
361 std::cout << "\nGDAL: inverse trafo =";
362 for (const auto &it : inverse_affine_transformation_)
363 std::cout << " " << it;
364 std::cout << "\nGDAL: raster offset =";
365 for (const auto &it : raster_offset_)
366 std::cout << " " << it;
367 std::cout << "\nGDAL: raster size =";
368 for (const auto &it : raster_size_)
369 std::cout << " " << it;
370 std::cout << std::endl;
371#endif
372
373 if (height_normalization_ != HeightNormalization::none) {
374 float shift = 0.;
375
376 if (height_normalization_ == HeightNormalization::minimum)
377 shift = *std::min_element(std::begin(raster_), std::end(raster_));
378 else if (height_normalization_ == HeightNormalization::maximum)
379 shift = *std::max_element(std::begin(raster_), std::end(raster_));
380 else {
381 Assert(height_normalization_ == HeightNormalization::average,
382 dealii::ExcInternalError());
383 const auto sum = std::reduce(std::begin(raster_), std::end(raster_));
384 shift = sum / raster_.size();
385 }
386
387 std::for_each(std::begin(raster_),
388 std::end(raster_),
389 [&](auto &element) { element -= shift; });
390 }
391
392#else
393 static constexpr auto message =
394 "ryujin has to be configured with GDAL support in order to read in "
395 "GeoTIFF images";
396 AssertThrow(false, dealii::ExcMessage(message));
397 __builtin_trap();
398#endif
399 }
400
401
402 DEAL_II_ALWAYS_INLINE inline std::array<double, 2>
403 apply_transformation(const double i, const double j) const
404 {
405 const auto &at = affine_transformation_;
406 const double x = at[0] + at[1] * i + at[2] * j;
407 const double y = at[3] + at[4] * i + at[5] * j;
408 return {x, y};
409 }
410
411
412 DEAL_II_ALWAYS_INLINE inline std::array<double, 2>
413 apply_inverse_transformation(const double x, const double y) const
414 {
415 const auto &iat = inverse_affine_transformation_;
416 const double i = iat[1] * (x - iat[0]) + iat[2] * (y - iat[3]);
417 const double j = iat[4] * (x - iat[0]) + iat[5] * (y - iat[3]);
418 return {i, j};
419 }
420
421 /* Runtime parameters: */
422
423 std::string filename_;
424
425 std::array<double, 6> transformation_;
426 bool transformation_allow_out_of_bounds_queries_;
427 bool transformation_use_geotiff_;
428 bool transformation_use_geotiff_origin_;
429 HeightNormalization height_normalization_;
430 double height_scaling_;
431
432 /* GDAL data structures: */
433
434 //
435 // We use a Lazy<t> wrapper for lazy initialization with efficient
436 // Schmidt's double checking. We simply ignore the bool type here.
437 //
438 Lazy<bool> geotiff_guard_;
439 mutable std::string driver_name_;
440 mutable std::string driver_projection_;
441 mutable std::array<double, 6> affine_transformation_;
442 mutable std::array<double, 6> inverse_affine_transformation_;
443 mutable std::array<int, 2> raster_offset_;
444 mutable std::array<int, 2> raster_size_;
445 mutable std::vector<float> raster_;
446 };
447} // namespace ryujin
const auto & raster_offset() const
const auto & raster_size() const
const auto & height_scaling() const
const auto & affine_transformation() const
GeoTIFFReader(const std::string subsection)
DEAL_II_ALWAYS_INLINE double compute_height(const dealii::Point< dim > &point) const
void ensure_initialized(const Callable &creator) const
#define ACCESSOR_READ_ONLY(member)