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