Coverage Report

Created: 2026-10-28 07:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/MapServer/src/kriging.c
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  MapServer
4
 * Purpose:  Kriging / Gaussian-Process layer interpolation via a localized
5
 *           Nearest-Neighbor Gaussian Process (NNGP). Each output pixel is
6
 *           predicted from its m nearest samples (ordinary kriging by default),
7
 *           so cost per pixel depends on the neighbor count, not the sample
8
 *           count -- the same prediction reduction NNGP uses to scale.
9
 *           Emits two byte bands: 1 = predictive mean, 2 = predictive std dev
10
 *           (the kriging standard error). Select with PROCESSING "BANDS=n".
11
 *
12
 * Author:   Hermes L. Herrera and the MapServer team.
13
 *
14
 ******************************************************************************
15
 * Copyright (c) 2024 Regents of the University of Minnesota.
16
 *
17
 * Permission is hereby granted, free of charge, to any person obtaining a
18
 * copy of this software and associated documentation files (the "Software"),
19
 * to deal in the Software without restriction, including without limitation
20
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
21
 * and/or sell copies of the Software, and to permit persons to whom the
22
 * Software is furnished to do so, subject to the following conditions:
23
 *
24
 * The above copyright notice and this permission notice shall be included in
25
 * all copies of this Software or works derived from this Software.
26
 *
27
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
28
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
30
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
33
 * DEALINGS IN THE SOFTWARE.
34
 *****************************************************************************/
35
36
#include "mapserver.h"
37
#include "kriging_nngp.h"
38
#include <float.h>
39
40
/* one sample point, used only for coincidence dedup */
41
typedef struct {
42
  float x, y, z;
43
} krSample;
44
45
0
static int krSampleCmp(const void *a, const void *b) {
46
0
  const krSample *p = (const krSample *)a, *q = (const krSample *)b;
47
0
  if (p->y != q->y)
48
0
    return p->y < q->y ? -1 : 1;
49
0
  if (p->x != q->x)
50
0
    return p->x < q->x ? -1 : 1;
51
0
  return 0;
52
0
}
53
54
0
static unsigned char krClamp(double v) {
55
0
  if (v <= 0.0)
56
0
    return 0;
57
0
  if (v >= 255.0)
58
0
    return 255;
59
0
  return (unsigned char)(v + 0.5);
60
0
}
61
62
void msKrigingProcessing(layerObj *layer,
63
0
                         interpolationProcessingParams *interpParams) {
64
0
  const char *key;
65
66
0
  key = msLayerGetProcessingKey(layer, "KRIGING_MODEL");
67
0
  if (key && !strcasecmp(key, "GAUSSIAN"))
68
0
    interpParams->kriging_model = KR_GAUSSIAN;
69
0
  else if (key && !strcasecmp(key, "SPHERICAL"))
70
0
    interpParams->kriging_model = KR_SPHERICAL;
71
0
  else
72
0
    interpParams->kriging_model = KR_EXPONENTIAL; /* Matern 1/2, the default */
73
74
0
  key = msLayerGetProcessingKey(layer, "KRIGING_TYPE");
75
0
  if (key && !strcasecmp(key, "SIMPLE"))
76
0
    interpParams->kriging_type = KR_SIMPLE;
77
0
  else
78
0
    interpParams->kriging_type = KR_ORDINARY;
79
80
0
  key = msLayerGetProcessingKey(layer, "KRIGING_NEIGHBORS");
81
0
  interpParams->kriging_neighbors = key ? atoi(key) : 16;
82
0
  if (interpParams->kriging_neighbors < 1)
83
0
    interpParams->kriging_neighbors = 1;
84
85
0
  key = msLayerGetProcessingKey(layer, "KRIGING_RANGE");
86
0
  interpParams->kriging_range = key ? atof(key) : 0.0; /* 0 => auto */
87
88
0
  key = msLayerGetProcessingKey(layer, "KRIGING_SILL");
89
0
  interpParams->kriging_sill = key ? atof(key) : 0.0; /* 0 => auto */
90
91
0
  key = msLayerGetProcessingKey(layer, "KRIGING_NUGGET");
92
0
  interpParams->kriging_nugget = key ? atof(key) : 0.0;
93
94
  /* NNGP works in the image frame; locality comes from the neighbor count, not
95
   * a border expansion, so the search rect is not grown (as with IDW
96
   * default).*/
97
0
  interpParams->expand_searchrect = 0;
98
0
}
99
100
void msKriging(float *xyz, int width, int height, int npoints,
101
               interpolationProcessingParams *interpParams,
102
0
               unsigned char *iMean, unsigned char *iSD) {
103
0
  krSample *pts;
104
0
  double *sx, *sy, *sz;
105
0
  double sigma2, range, nugget, sum, mean;
106
0
  int n, i, m, px, py;
107
0
  kr_grid grid;
108
0
  kr_knn kn;
109
0
  int *nbidx;
110
0
  double *nbd2, *A, *k0, *w;
111
0
  int model = interpParams->kriging_model;
112
0
  int ktype = interpParams->kriging_type;
113
114
0
  if (npoints <= 0)
115
0
    return;
116
117
  /* Collapse coincident samples: two points on the same pixel give identical
118
   * rows in K, hence a singular system when nugget == 0. msInterpolationDataset
119
   * hands us a running per-pixel accumulation, so the max over a duplicate
120
   * group is that pixel's final total. Sort by (y,x) to bring duplicates
121
   * together. */
122
0
  pts = (krSample *)msSmallMalloc((size_t)npoints * sizeof(krSample));
123
0
  for (i = 0; i < npoints; i++) {
124
0
    pts[i].x = xyz[i * 3];
125
0
    pts[i].y = xyz[i * 3 + 1];
126
0
    pts[i].z = xyz[i * 3 + 2];
127
0
  }
128
0
  qsort(pts, npoints, sizeof(krSample), krSampleCmp);
129
130
0
  sx = (double *)msSmallMalloc((size_t)npoints * sizeof(double));
131
0
  sy = (double *)msSmallMalloc((size_t)npoints * sizeof(double));
132
0
  sz = (double *)msSmallMalloc((size_t)npoints * sizeof(double));
133
0
  n = 0;
134
0
  for (i = 0; i < npoints;) {
135
0
    int j = i;
136
0
    double best = pts[i].z;
137
0
    while (j < npoints && pts[j].x == pts[i].x && pts[j].y == pts[i].y) {
138
0
      if (pts[j].z > best)
139
0
        best = pts[j].z;
140
0
      j++;
141
0
    }
142
0
    sx[n] = pts[i].x;
143
0
    sy[n] = pts[i].y;
144
0
    sz[n] = best;
145
0
    n++;
146
0
    i = j;
147
0
  }
148
0
  free(pts);
149
150
  /* covariance parameters: honour the PROCESSING keys, else auto-fit them. */
151
0
  sigma2 = interpParams->kriging_sill;
152
0
  if (sigma2 <= 0.0) { /* auto sill = sample variance of the values */
153
0
    sum = 0.0;
154
0
    for (i = 0; i < n; i++)
155
0
      sum += sz[i];
156
0
    mean = sum / n;
157
0
    sigma2 = 0.0;
158
0
    for (i = 0; i < n; i++)
159
0
      sigma2 += (sz[i] - mean) * (sz[i] - mean);
160
0
    sigma2 = n > 1 ? sigma2 / (n - 1) : 1.0;
161
0
    if (sigma2 <= 0.0)
162
0
      sigma2 = 1.0;
163
0
  }
164
0
  range = interpParams->kriging_range;
165
0
  if (range <= 0.0) /* auto range ~ 1/3 of the image diagonal (in pixels) */
166
0
    range = 0.33 * sqrt((double)width * width + (double)height * height);
167
0
  nugget = interpParams->kriging_nugget;
168
169
0
  m = interpParams->kriging_neighbors;
170
0
  if (m > n)
171
0
    m = n;
172
173
  /* Build the neighbour index once, and size the per-solve scratch for the
174
   * bordered system (m+1) up front so the per-pixel loop never allocates. */
175
0
  if (kr_grid_build(&grid, sx, sy, n) != 0) {
176
0
    free(sx);
177
0
    free(sy);
178
0
    free(sz);
179
0
    return;
180
0
  }
181
0
  nbidx = (int *)msSmallMalloc((size_t)m * sizeof(int));
182
0
  nbd2 = (double *)msSmallMalloc((size_t)m * sizeof(double));
183
0
  A = (double *)msSmallMalloc((size_t)(m + 1) * (m + 1) * sizeof(double));
184
0
  k0 = (double *)msSmallMalloc((size_t)(m + 1) * sizeof(double));
185
0
  w = (double *)msSmallMalloc((size_t)(m + 1) * sizeof(double));
186
0
  kn.idx = nbidx;
187
0
  kn.d2 = nbd2;
188
0
  kn.cap = m;
189
190
  /* Predict every output pixel from its m nearest samples. The pixels are
191
   * independent, so this parallelises with an OpenMP "parallel for" once each
192
   * thread is given its own kn/A/k0/w scratch. */
193
0
  for (py = 0; py < height; py++) {
194
0
    for (px = 0; px < width; px++) {
195
0
      double pmean, pvar;
196
0
      kr_grid_knn(&grid, sx, sy, (double)px, (double)py, m, &kn);
197
0
      if (kr_predict(sx, sy, sz, kn.idx, kn.count, (double)px, (double)py,
198
0
                     model, ktype, sigma2, range, nugget, A, k0, w, &pmean,
199
0
                     &pvar) != 0)
200
0
        continue; /* leave no-data (0) on a singular local system */
201
0
      iMean[py * width + px] = krClamp(pmean);
202
0
      iSD[py * width + px] = krClamp(sqrt(pvar));
203
0
    }
204
0
  }
205
206
0
  free(nbidx);
207
0
  free(nbd2);
208
0
  free(A);
209
0
  free(k0);
210
0
  free(w);
211
0
  kr_grid_free(&grid);
212
0
  free(sx);
213
0
  free(sy);
214
0
  free(sz);
215
0
}