/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 | } |