Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: GDAL
4 : * Purpose: gdal "raster pixelinfo" subcommand
5 : * Author: Even Rouault <even dot rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "gdalalg_raster_pixel_info.h"
14 :
15 : #include "cpl_conv.h"
16 : #include "cpl_json.h"
17 : #include "cpl_minixml.h"
18 : #include "gdal_priv.h"
19 : #include "ogr_spatialref.h"
20 :
21 : #include <algorithm>
22 : #include <cmath>
23 :
24 : //! @cond Doxygen_Suppress
25 :
26 : #ifndef _
27 : #define _(x) (x)
28 : #endif
29 :
30 : /************************************************************************/
31 : /* GDALRasterPixelInfoAlgorithm::GDALRasterPixelInfoAlgorithm() */
32 : /************************************************************************/
33 :
34 25 : GDALRasterPixelInfoAlgorithm::GDALRasterPixelInfoAlgorithm()
35 25 : : GDALAlgorithm(NAME, DESCRIPTION, HELP_URL)
36 : {
37 25 : AddOutputFormatArg(&m_format)
38 25 : .SetDefault("geojson")
39 25 : .SetChoices("geojson", "csv")
40 25 : .SetHiddenChoices("json")
41 25 : .SetDefault(m_format);
42 25 : AddOpenOptionsArg(&m_openOptions);
43 25 : AddInputFormatsArg(&m_inputFormats)
44 50 : .AddMetadataItem(GAAMDI_REQUIRED_CAPABILITIES, {GDAL_DCAP_RASTER});
45 25 : AddInputDatasetArg(&m_dataset, GDAL_OF_RASTER).AddAlias("dataset");
46 25 : AddOutputStringArg(&m_output);
47 :
48 25 : AddBandArg(&m_band);
49 : AddArg("overview", 0, _("Which overview level of source file must be used"),
50 50 : &m_overview)
51 25 : .SetMinValueIncluded(0);
52 :
53 50 : AddArg("position", 'p', _("Pixel position"), &m_pos)
54 50 : .AddAlias("pos")
55 50 : .SetMetaVar("<column,line> or <X,Y>")
56 25 : .SetPositional()
57 : .AddValidationAction(
58 19 : [this]
59 : {
60 18 : if ((m_pos.size() % 2) != 0)
61 : {
62 1 : ReportError(CE_Failure, CPLE_IllegalArg,
63 : "An even number of values must be specified "
64 : "for 'position' argument");
65 1 : return false;
66 : }
67 17 : return true;
68 25 : });
69 50 : AddArg("position-crs", 0, _("CRS of position"), &m_posCrs)
70 100 : .SetIsCRSArg(false, {"pixel", "dataset"})
71 25 : .SetDefault("pixel")
72 25 : .AddHiddenAlias("l_srs");
73 :
74 : AddArg("resampling", 'r', _("Resampling algorithm for interpolation"),
75 50 : &m_resampling)
76 25 : .SetDefault(m_resampling)
77 25 : .SetChoices("nearest", "bilinear", "cubic", "cubicspline")
78 25 : .SetHiddenChoices("near");
79 :
80 25 : AddValidationAction(
81 53 : [this]
82 : {
83 24 : if (auto poSrcDS = m_dataset.GetDatasetRef())
84 : {
85 : const int nOvrCount =
86 24 : poSrcDS->GetRasterBand(1)->GetOverviewCount();
87 27 : if (m_overview >= 0 && poSrcDS->GetRasterCount() > 0 &&
88 3 : m_overview >= nOvrCount)
89 : {
90 2 : if (nOvrCount == 0)
91 : {
92 1 : ReportError(
93 : CE_Failure, CPLE_IllegalArg,
94 : "Source dataset has no overviews. "
95 : "Argument 'overview' must not be specified.");
96 : }
97 : else
98 : {
99 1 : ReportError(
100 : CE_Failure, CPLE_IllegalArg,
101 : "Source dataset has only %d overview level%s. "
102 : "'overview' "
103 : "value must be strictly lower than this number.",
104 : nOvrCount, nOvrCount > 1 ? "s" : "");
105 : }
106 2 : return false;
107 : }
108 : }
109 22 : return true;
110 : });
111 25 : }
112 :
113 : /************************************************************************/
114 : /* GDALRasterPixelInfoAlgorithm::PrintLine() */
115 : /************************************************************************/
116 :
117 12 : void GDALRasterPixelInfoAlgorithm::PrintLine(const std::string &str)
118 : {
119 12 : if (IsCalledFromCommandLine())
120 : {
121 2 : printf("%s\n", str.c_str());
122 : }
123 : else
124 : {
125 10 : m_output += str;
126 10 : m_output += '\n';
127 : }
128 12 : }
129 :
130 : /************************************************************************/
131 : /* GDALRasterPixelInfoAlgorithm::RunImpl() */
132 : /************************************************************************/
133 :
134 20 : bool GDALRasterPixelInfoAlgorithm::RunImpl(GDALProgressFunc, void *)
135 : {
136 20 : auto poSrcDS = m_dataset.GetDatasetRef();
137 20 : CPLAssert(poSrcDS);
138 :
139 20 : if (m_pos.empty() && !IsCalledFromCommandLine())
140 : {
141 1 : ReportError(CE_Failure, CPLE_AppDefined,
142 : "Argument 'position' must be specified.");
143 1 : return false;
144 : }
145 :
146 : const GDALRIOResampleAlg eInterpolation =
147 19 : GDALRasterIOGetResampleAlg(m_resampling.c_str());
148 :
149 19 : const auto poSrcCRS = poSrcDS->GetSpatialRef();
150 19 : double adfGeoTransform[6] = {0, 0, 0, 0, 0, 0};
151 19 : const bool bHasGT = poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None;
152 19 : double adfInvGeoTransform[6] = {0, 0, 0, 0, 0, 0};
153 :
154 19 : if (m_posCrs != "pixel")
155 : {
156 5 : if (!poSrcCRS)
157 : {
158 1 : ReportError(CE_Failure, CPLE_AppDefined,
159 : "Dataset has no CRS. Only 'position-crs' = 'pixel' is "
160 : "supported.");
161 1 : return false;
162 : }
163 :
164 4 : if (!bHasGT)
165 : {
166 1 : ReportError(CE_Failure, CPLE_AppDefined, "Cannot get geotransform");
167 1 : return false;
168 : }
169 :
170 3 : if (!GDALInvGeoTransform(adfGeoTransform, adfInvGeoTransform))
171 : {
172 1 : ReportError(CE_Failure, CPLE_AppDefined,
173 : "Cannot invert geotransform");
174 1 : return false;
175 : }
176 : }
177 :
178 16 : std::unique_ptr<OGRCoordinateTransformation> poCT;
179 16 : if (m_posCrs != "pixel" && m_posCrs != "dataset")
180 : {
181 1 : OGRSpatialReference oUserCRS;
182 1 : oUserCRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
183 : // Already validated due SetIsCRSArg()
184 1 : CPL_IGNORE_RET_VAL(oUserCRS.SetFromUserInput(m_posCrs.c_str()));
185 1 : poCT.reset(OGRCreateCoordinateTransformation(&oUserCRS, poSrcCRS));
186 1 : if (!poCT)
187 0 : return false;
188 : }
189 :
190 16 : if (m_band.empty())
191 : {
192 32 : for (int i = 1; i <= poSrcDS->GetRasterCount(); ++i)
193 16 : m_band.push_back(i);
194 : }
195 :
196 16 : if (m_format == "csv")
197 : {
198 12 : std::string line = "input_x,input_y,extra_input,column,line";
199 12 : for (int nBand : m_band)
200 : {
201 : auto hBand =
202 6 : GDALRasterBand::ToHandle(poSrcDS->GetRasterBand(nBand));
203 6 : const bool bIsComplex = CPL_TO_BOOL(
204 : GDALDataTypeIsComplex(GDALGetRasterDataType(hBand)));
205 6 : if (bIsComplex)
206 : line +=
207 : CPLSPrintf(",band_%d_real_value,band_%d_imaginary_value",
208 2 : nBand, nBand);
209 : else
210 : {
211 : line += CPLSPrintf(",band_%d_raw_value,band_%d_unscaled_value",
212 4 : nBand, nBand);
213 : }
214 : }
215 6 : PrintLine(line);
216 : }
217 :
218 : const bool isInteractive =
219 16 : m_pos.empty() && IsCalledFromCommandLine() && CPLIsInteractive(stdin);
220 :
221 32 : CPLJSONObject oCollection;
222 16 : oCollection.Add("type", "FeatureCollection");
223 16 : std::unique_ptr<OGRCoordinateTransformation> poCTToWGS84;
224 16 : bool canOutputGeoJSONGeom = false;
225 16 : if (poSrcCRS && bHasGT)
226 : {
227 14 : const char *pszAuthName = poSrcCRS->GetAuthorityName(nullptr);
228 14 : const char *pszAuthCode = poSrcCRS->GetAuthorityCode(nullptr);
229 14 : if (pszAuthName && pszAuthCode && EQUAL(pszAuthName, "EPSG"))
230 : {
231 13 : canOutputGeoJSONGeom = true;
232 26 : CPLJSONObject jCRS;
233 13 : CPLJSONObject oProperties;
234 13 : if (EQUAL(pszAuthCode, "4326"))
235 2 : oProperties.Add("name", "urn:ogc:def:crs:OGC:1.3:CRS84");
236 : else
237 11 : oProperties.Add(
238 : "name",
239 22 : std::string("urn:ogc:def:crs:EPSG::").append(pszAuthCode));
240 13 : jCRS.Add("type", "name");
241 13 : jCRS.Add("properties", oProperties);
242 26 : oCollection.Add("crs", jCRS);
243 : }
244 : else
245 : {
246 2 : OGRSpatialReference oCRS;
247 1 : oCRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
248 1 : oCRS.importFromEPSG(4326);
249 1 : poCTToWGS84.reset(
250 : OGRCreateCoordinateTransformation(poSrcCRS, &oCRS));
251 1 : if (poCTToWGS84)
252 : {
253 1 : canOutputGeoJSONGeom = true;
254 2 : CPLJSONObject jCRS;
255 1 : CPLJSONObject oProperties;
256 1 : oProperties.Add("name", "urn:ogc:def:crs:OGC:1.3:CRS84");
257 1 : jCRS.Add("type", "name");
258 1 : jCRS.Add("properties", oProperties);
259 1 : oCollection.Add("crs", jCRS);
260 : }
261 : }
262 : }
263 32 : CPLJSONArray oFeatures;
264 16 : oCollection.Add("features", oFeatures);
265 :
266 : char szLine[1024];
267 16 : int nLine = 0;
268 16 : size_t iVal = 0;
269 2 : do
270 : {
271 18 : double x = 0, y = 0;
272 18 : std::string osExtraContent;
273 18 : if (iVal + 1 < m_pos.size())
274 : {
275 14 : x = m_pos[iVal++];
276 14 : y = m_pos[iVal++];
277 : }
278 : else
279 : {
280 4 : if (CPLIsInteractive(stdin))
281 : {
282 0 : if (m_posCrs != "pixel")
283 : {
284 0 : fprintf(stderr, "Enter X Y values separated by space, and "
285 : "press Return.\n");
286 : }
287 : else
288 : {
289 0 : fprintf(stderr,
290 : "Enter pixel line values separated by space, "
291 : "and press Return.\n");
292 : }
293 : }
294 :
295 4 : if (fgets(szLine, sizeof(szLine) - 1, stdin) && szLine[0] != '\n')
296 : {
297 2 : const CPLStringList aosTokens(CSLTokenizeString(szLine));
298 2 : const int nCount = aosTokens.size();
299 :
300 2 : ++nLine;
301 2 : if (nCount < 2)
302 : {
303 0 : fprintf(stderr, "Not enough values at line %d\n", nLine);
304 0 : return false;
305 : }
306 : else
307 : {
308 2 : x = CPLAtof(aosTokens[0]);
309 2 : y = CPLAtof(aosTokens[1]);
310 :
311 6 : for (int i = 2; i < nCount; ++i)
312 : {
313 4 : if (!osExtraContent.empty())
314 2 : osExtraContent += ' ';
315 4 : osExtraContent += aosTokens[i];
316 : }
317 4 : while (!osExtraContent.empty() &&
318 2 : isspace(static_cast<int>(osExtraContent.back())))
319 : {
320 0 : osExtraContent.pop_back();
321 : }
322 : }
323 : }
324 : else
325 : {
326 2 : break;
327 : }
328 : }
329 :
330 16 : const double xOri = x;
331 16 : const double yOri = y;
332 16 : double dfPixel{0}, dfLine{0};
333 :
334 16 : if (poCT)
335 : {
336 1 : if (!poCT->Transform(1, &x, &y, nullptr))
337 0 : return false;
338 : }
339 :
340 16 : if (m_posCrs != "pixel")
341 : {
342 2 : GDALApplyGeoTransform(adfInvGeoTransform, x, y, &dfPixel, &dfLine);
343 : }
344 : else
345 : {
346 14 : dfPixel = x;
347 14 : dfLine = y;
348 : }
349 : const int iPixel = static_cast<int>(
350 32 : std::clamp(std::floor(dfPixel), static_cast<double>(INT_MIN),
351 16 : static_cast<double>(INT_MAX)));
352 : const int iLine = static_cast<int>(
353 32 : std::clamp(std::floor(dfLine), static_cast<double>(INT_MIN),
354 16 : static_cast<double>(INT_MAX)));
355 :
356 16 : std::string line;
357 16 : CPLJSONObject oFeature;
358 16 : CPLJSONObject oProperties;
359 16 : if (m_format == "csv")
360 : {
361 6 : line = CPLSPrintf("%.17g,%.17g", xOri, yOri);
362 6 : line += ",\"";
363 6 : line += CPLString(osExtraContent).replaceAll('"', "\"\"");
364 6 : line += '"';
365 6 : line += CPLSPrintf(",%.17g,%.17g", dfPixel, dfLine);
366 : }
367 : else
368 : {
369 10 : oFeature.Add("type", "Feature");
370 10 : oFeature.Add("properties", oProperties);
371 : {
372 10 : CPLJSONArray oArray;
373 10 : oArray.Add(xOri);
374 10 : oArray.Add(yOri);
375 10 : oProperties.Add("input_coordinate", oArray);
376 : }
377 10 : if (!osExtraContent.empty())
378 1 : oProperties.Add("extra_content", osExtraContent);
379 10 : oProperties.Add("column", dfPixel);
380 10 : oProperties.Add("line", dfLine);
381 : }
382 :
383 16 : CPLJSONArray oBands;
384 :
385 32 : for (int nBand : m_band)
386 : {
387 16 : CPLJSONObject oBand;
388 16 : oBand.Add("band_number", nBand);
389 :
390 : auto hBand =
391 16 : GDALRasterBand::ToHandle(poSrcDS->GetRasterBand(nBand));
392 :
393 16 : int iPixelToQuery = iPixel;
394 16 : int iLineToQuery = iLine;
395 :
396 16 : double dfPixelToQuery = dfPixel;
397 16 : double dfLineToQuery = dfLine;
398 :
399 16 : if (m_overview >= 0 && hBand != nullptr)
400 : {
401 1 : GDALRasterBandH hOvrBand = GDALGetOverview(hBand, m_overview);
402 1 : if (hOvrBand != nullptr)
403 : {
404 1 : int nOvrXSize = GDALGetRasterBandXSize(hOvrBand);
405 1 : int nOvrYSize = GDALGetRasterBandYSize(hOvrBand);
406 1 : iPixelToQuery = static_cast<int>(
407 1 : 0.5 +
408 1 : 1.0 * iPixel / poSrcDS->GetRasterXSize() * nOvrXSize);
409 1 : iLineToQuery = static_cast<int>(
410 1 : 0.5 +
411 1 : 1.0 * iLine / poSrcDS->GetRasterYSize() * nOvrYSize);
412 1 : if (iPixelToQuery >= nOvrXSize)
413 0 : iPixelToQuery = nOvrXSize - 1;
414 1 : if (iLineToQuery >= nOvrYSize)
415 0 : iLineToQuery = nOvrYSize - 1;
416 1 : dfPixelToQuery =
417 1 : dfPixel / poSrcDS->GetRasterXSize() * nOvrXSize;
418 1 : dfLineToQuery =
419 1 : dfLine / poSrcDS->GetRasterYSize() * nOvrYSize;
420 : }
421 : else
422 : {
423 0 : ReportError(CE_Failure, CPLE_AppDefined,
424 : "Cannot get overview %d of band %d", m_overview,
425 : nBand);
426 0 : return false;
427 : }
428 1 : hBand = hOvrBand;
429 : }
430 :
431 16 : double adfPixel[2] = {0, 0};
432 16 : const bool bIsComplex = CPL_TO_BOOL(
433 : GDALDataTypeIsComplex(GDALGetRasterDataType(hBand)));
434 : int bIgnored;
435 16 : const double dfOffset = GDALGetRasterOffset(hBand, &bIgnored);
436 16 : const double dfScale = GDALGetRasterScale(hBand, &bIgnored);
437 16 : if (GDALRasterInterpolateAtPoint(
438 : hBand, dfPixelToQuery, dfLineToQuery, eInterpolation,
439 16 : &adfPixel[0], &adfPixel[1]) == CE_None)
440 : {
441 14 : if (!bIsComplex)
442 : {
443 12 : const double dfUnscaledVal =
444 12 : adfPixel[0] * dfScale + dfOffset;
445 12 : if (m_format == "csv")
446 : {
447 3 : line += CPLSPrintf(",%.17g", adfPixel[0]);
448 3 : line += CPLSPrintf(",%.17g", dfUnscaledVal);
449 : }
450 : else
451 : {
452 9 : if (GDALDataTypeIsInteger(GDALGetRasterDataType(hBand)))
453 : {
454 8 : oBand.Add("raw_value",
455 8 : static_cast<GInt64>(adfPixel[0]));
456 : }
457 : else
458 : {
459 1 : oBand.Add("raw_value", adfPixel[0]);
460 : }
461 :
462 9 : oBand.Add("unscaled_value", dfUnscaledVal);
463 : }
464 : }
465 : else
466 : {
467 2 : if (m_format == "csv")
468 : {
469 : line += CPLSPrintf(",%.17g,%.17g", adfPixel[0],
470 1 : adfPixel[1]);
471 : }
472 : else
473 : {
474 1 : CPLJSONObject oValue;
475 1 : oValue.Add("real", adfPixel[0]);
476 1 : oValue.Add("imaginary", adfPixel[1]);
477 1 : oBand.Add("value", oValue);
478 : }
479 : }
480 : }
481 2 : else if (m_format == "csv")
482 : {
483 2 : line += ",,";
484 : }
485 :
486 : // Request location info for this location (just a few drivers,
487 : // like the VRT driver actually supports this).
488 32 : CPLString osItem;
489 16 : osItem.Printf("Pixel_%d_%d", iPixelToQuery, iLineToQuery);
490 :
491 16 : if (const char *pszLI =
492 16 : GDALGetMetadataItem(hBand, osItem, "LocationInfo"))
493 : {
494 2 : CPLXMLTreeCloser oTree(CPLParseXMLString(pszLI));
495 :
496 2 : if (oTree && oTree->psChild != nullptr &&
497 3 : oTree->eType == CXT_Element &&
498 1 : EQUAL(oTree->pszValue, "LocationInfo"))
499 : {
500 1 : CPLJSONArray oFiles;
501 :
502 1 : for (const CPLXMLNode *psNode = oTree->psChild;
503 2 : psNode != nullptr; psNode = psNode->psNext)
504 : {
505 1 : if (psNode->eType == CXT_Element &&
506 1 : EQUAL(psNode->pszValue, "File") &&
507 1 : psNode->psChild != nullptr)
508 : {
509 2 : char *pszUnescaped = CPLUnescapeString(
510 1 : psNode->psChild->pszValue, nullptr, CPLES_XML);
511 1 : oFiles.Add(pszUnescaped);
512 1 : CPLFree(pszUnescaped);
513 : }
514 : }
515 :
516 1 : oBand.Add("files", oFiles);
517 : }
518 : else
519 : {
520 0 : oBand.Add("location_info", pszLI);
521 : }
522 : }
523 :
524 16 : oBands.Add(oBand);
525 : }
526 :
527 16 : if (m_format == "csv")
528 : {
529 6 : PrintLine(line);
530 : }
531 : else
532 : {
533 10 : oProperties.Add("bands", oBands);
534 :
535 10 : if (canOutputGeoJSONGeom)
536 : {
537 9 : x = dfPixel;
538 9 : y = dfLine;
539 :
540 9 : GDALApplyGeoTransform(adfGeoTransform, x, y, &x, &y);
541 :
542 9 : if (poCTToWGS84)
543 1 : poCTToWGS84->Transform(1, &x, &y);
544 :
545 18 : CPLJSONObject oGeometry;
546 9 : oFeature.Add("geometry", oGeometry);
547 9 : oGeometry.Add("type", "Point");
548 9 : CPLJSONArray oCoordinates;
549 9 : oCoordinates.Add(x);
550 9 : oCoordinates.Add(y);
551 9 : oGeometry.Add("coordinates", oCoordinates);
552 : }
553 : else
554 : {
555 1 : oFeature.AddNull("geometry");
556 : }
557 :
558 10 : if (isInteractive)
559 : {
560 0 : CPLJSONDocument oDoc;
561 0 : oDoc.SetRoot(oFeature);
562 0 : printf("%s\n", oDoc.SaveAsString().c_str());
563 : }
564 : else
565 : {
566 10 : oFeatures.Add(oFeature);
567 : }
568 : }
569 :
570 16 : } while (m_pos.empty() || iVal + 1 < m_pos.size());
571 :
572 16 : if (m_format != "csv" && !isInteractive)
573 : {
574 10 : CPLJSONDocument oDoc;
575 10 : oDoc.SetRoot(oCollection);
576 10 : m_output = oDoc.SaveAsString();
577 : }
578 :
579 16 : return true;
580 : }
581 :
582 : //! @endcond
|