1use geonative_core::raster::{Band, RasterTile, ResamplingMethod};
32
33use crate::raster::pixel;
34
35pub fn sample(
41 band: &Band,
42 width: u32,
43 height: u32,
44 x: f64,
45 y: f64,
46 method: ResamplingMethod,
47) -> Option<f64> {
48 let w = width as usize;
49 let h = height as usize;
50 match method {
51 ResamplingMethod::Nearest => sample_nearest(band, w, h, x, y),
52 ResamplingMethod::Bilinear => sample_bilinear(band, w, h, x, y),
53 ResamplingMethod::Cubic => sample_cubic(band, w, h, x, y),
54 ResamplingMethod::Lanczos => sample_lanczos(band, w, h, x, y, 3),
55 ResamplingMethod::Average | ResamplingMethod::Mode => sample_bilinear(band, w, h, x, y),
59 _ => sample_nearest(band, w, h, x, y),
60 }
61}
62
63fn sample_nearest(band: &Band, w: usize, h: usize, x: f64, y: f64) -> Option<f64> {
64 let c = x.floor() as isize;
65 let r = y.floor() as isize;
66 if c < 0 || r < 0 || (c as usize) >= w || (r as usize) >= h {
67 return None;
68 }
69 pixel::read(band, c as usize, r as usize, w)
70}
71
72fn sample_bilinear(band: &Band, w: usize, h: usize, x: f64, y: f64) -> Option<f64> {
73 let xc = x - 0.5;
76 let yc = y - 0.5;
77 let c0 = xc.floor() as isize;
78 let r0 = yc.floor() as isize;
79 let dx = xc - c0 as f64;
80 let dy = yc - r0 as f64;
81 let p00 = read_clamped(band, c0, r0, w, h)?;
82 let p10 = read_clamped(band, c0 + 1, r0, w, h)?;
83 let p01 = read_clamped(band, c0, r0 + 1, w, h)?;
84 let p11 = read_clamped(band, c0 + 1, r0 + 1, w, h)?;
85 let top = p00 * (1.0 - dx) + p10 * dx;
86 let bot = p01 * (1.0 - dx) + p11 * dx;
87 Some(top * (1.0 - dy) + bot * dy)
88}
89
90fn sample_cubic(band: &Band, w: usize, h: usize, x: f64, y: f64) -> Option<f64> {
91 let xc = x - 0.5;
93 let yc = y - 0.5;
94 let c0 = xc.floor() as isize;
95 let r0 = yc.floor() as isize;
96 let dx = xc - c0 as f64;
97 let dy = yc - r0 as f64;
98
99 let mut rows = [0.0f64; 4];
100 for (j, row_slot) in rows.iter_mut().enumerate() {
101 let r = r0 + j as isize - 1;
102 let mut row = [0.0f64; 4];
103 for (i, slot) in row.iter_mut().enumerate() {
104 let c = c0 + i as isize - 1;
105 *slot = read_clamped(band, c, r, w, h).unwrap_or(0.0);
106 }
107 *row_slot = cubic_interp(&row, dx);
108 }
109 Some(cubic_interp(&rows, dy))
110}
111
112fn cubic_interp(p: &[f64; 4], t: f64) -> f64 {
114 let t2 = t * t;
115 let t3 = t2 * t;
116 0.5 * ((2.0 * p[1])
117 + (-p[0] + p[2]) * t
118 + (2.0 * p[0] - 5.0 * p[1] + 4.0 * p[2] - p[3]) * t2
119 + (-p[0] + 3.0 * p[1] - 3.0 * p[2] + p[3]) * t3)
120}
121
122fn sample_lanczos(band: &Band, w: usize, h: usize, x: f64, y: f64, a: i32) -> Option<f64> {
123 let xc = x - 0.5;
124 let yc = y - 0.5;
125 let cf = xc.floor();
126 let rf = yc.floor();
127 let dx = xc - cf;
128 let dy = yc - rf;
129
130 let mut num = 0.0f64;
131 let mut den = 0.0f64;
132 for j in -(a - 1)..=a {
133 let r = rf as isize + j as isize;
134 let ly = lanczos_kernel(j as f64 - dy, a);
135 for i in -(a - 1)..=a {
136 let c = cf as isize + i as isize;
137 let lx = lanczos_kernel(i as f64 - dx, a);
138 let w_ij = lx * ly;
139 let p = read_clamped(band, c, r, w, h).unwrap_or(0.0);
140 num += w_ij * p;
141 den += w_ij;
142 }
143 }
144 if den.abs() < 1e-12 {
145 None
146 } else {
147 Some(num / den)
148 }
149}
150
151fn lanczos_kernel(x: f64, a: i32) -> f64 {
152 use std::f64::consts::PI;
153 if x.abs() < 1e-12 {
154 return 1.0;
155 }
156 let af = a as f64;
157 if x.abs() >= af {
158 return 0.0;
159 }
160 let px = PI * x;
161 (af * (px).sin() * (px / af).sin()) / (px * px)
162}
163
164fn read_clamped(band: &Band, c: isize, r: isize, w: usize, h: usize) -> Option<f64> {
165 let cc = c.clamp(0, w as isize - 1) as usize;
166 let rr = r.clamp(0, h as isize - 1) as usize;
167 pixel::read(band, cc, rr, w)
168}
169
170pub fn resample_tile(
175 source: &RasterTile,
176 target_width: u32,
177 target_height: u32,
178 method: ResamplingMethod,
179) -> RasterTile {
180 let sw = source.width as f64;
181 let sh = source.height as f64;
182 let tw = target_width.max(1);
183 let th = target_height.max(1);
184 let sx = sw / tw as f64;
185 let sy = sh / th as f64;
186
187 let mut bands = Vec::with_capacity(source.bands.len());
188 for src_band in &source.bands {
189 let mut dst_band = Band::new(
190 src_band.descriptor.clone(),
191 vec![0u8; (tw as usize) * (th as usize) * src_band.descriptor.dtype.size_bytes()],
192 );
193
194 let is_downsample = sx > 1.0 || sy > 1.0;
196 let use_area_kernel =
197 matches!(method, ResamplingMethod::Average | ResamplingMethod::Mode) && is_downsample;
198
199 for ty in 0..th as usize {
200 for tx in 0..tw as usize {
201 let value = if use_area_kernel {
202 let x0 = tx as f64 * sx;
203 let x1 = (tx + 1) as f64 * sx;
204 let y0 = ty as f64 * sy;
205 let y1 = (ty + 1) as f64 * sy;
206 match method {
207 ResamplingMethod::Average => area_average(
208 src_band,
209 source.width as usize,
210 source.height as usize,
211 x0,
212 y0,
213 x1,
214 y1,
215 ),
216 ResamplingMethod::Mode => area_mode(
217 src_band,
218 source.width as usize,
219 source.height as usize,
220 x0,
221 y0,
222 x1,
223 y1,
224 ),
225 _ => unreachable!(),
226 }
227 } else {
228 let sxp = (tx as f64 + 0.5) * sx;
230 let syp = (ty as f64 + 0.5) * sy;
231 sample(src_band, source.width, source.height, sxp, syp, method)
232 };
233 if let Some(v) = value {
234 pixel::write(&mut dst_band, tx, ty, tw as usize, v);
235 }
236 }
237 }
238 bands.push(dst_band);
239 }
240
241 let mut gt = source.geo_transform;
244 gt.pixel_size[0] *= sx;
245 gt.pixel_size[1] *= sy;
246
247 RasterTile {
248 width: tw,
249 height: th,
250 bands,
251 geo_transform: gt,
252 crs: source.crs.clone(),
253 }
254}
255
256fn area_average(
257 band: &Band,
258 w: usize,
259 h: usize,
260 x0: f64,
261 y0: f64,
262 x1: f64,
263 y1: f64,
264) -> Option<f64> {
265 let c0 = (x0.floor() as isize).max(0);
266 let c1 = ((x1.ceil() as isize) - 1).min(w as isize - 1);
267 let r0 = (y0.floor() as isize).max(0);
268 let r1 = ((y1.ceil() as isize) - 1).min(h as isize - 1);
269 if c1 < c0 || r1 < r0 {
270 return None;
271 }
272 let mut sum = 0.0f64;
273 let mut count = 0usize;
274 for r in r0..=r1 {
275 for c in c0..=c1 {
276 if let Some(v) = pixel::read(band, c as usize, r as usize, w) {
277 sum += v;
278 count += 1;
279 }
280 }
281 }
282 if count == 0 {
283 None
284 } else {
285 Some(sum / count as f64)
286 }
287}
288
289fn area_mode(band: &Band, w: usize, h: usize, x0: f64, y0: f64, x1: f64, y1: f64) -> Option<f64> {
290 use std::collections::HashMap;
291 let c0 = (x0.floor() as isize).max(0);
292 let c1 = ((x1.ceil() as isize) - 1).min(w as isize - 1);
293 let r0 = (y0.floor() as isize).max(0);
294 let r1 = ((y1.ceil() as isize) - 1).min(h as isize - 1);
295 if c1 < c0 || r1 < r0 {
296 return None;
297 }
298 let mut counts: HashMap<u64, u32> = HashMap::new();
299 for r in r0..=r1 {
300 for c in c0..=c1 {
301 if let Some(v) = pixel::read(band, c as usize, r as usize, w) {
302 *counts.entry(v.to_bits()).or_insert(0) += 1;
305 }
306 }
307 }
308 counts
309 .into_iter()
310 .max_by_key(|&(_, c)| c)
311 .map(|(bits, _)| f64::from_bits(bits))
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use geonative_core::raster::{BandDescriptor, GeoTransform, PixelType};
318 use geonative_core::Crs;
319
320 fn u8_tile(width: u32, height: u32, fill: u8) -> RasterTile {
321 let pixels = (width as usize) * (height as usize);
322 RasterTile {
323 width,
324 height,
325 bands: vec![Band::new(
326 BandDescriptor::new(Some("v".into()), PixelType::U8),
327 vec![fill; pixels],
328 )],
329 geo_transform: GeoTransform::north_up(0.0, 0.0, 1.0, 1.0),
330 crs: Crs::Epsg(3857),
331 }
332 }
333
334 fn gradient_tile() -> RasterTile {
336 let mut data = Vec::with_capacity(16);
337 for _r in 0..4u8 {
338 for c in 0..4u8 {
339 data.push(c * 80); }
341 }
342 RasterTile {
343 width: 4,
344 height: 4,
345 bands: vec![Band::new(
346 BandDescriptor::new(Some("v".into()), PixelType::U8),
347 data,
348 )],
349 geo_transform: GeoTransform::north_up(0.0, 0.0, 1.0, 1.0),
350 crs: Crs::Epsg(3857),
351 }
352 }
353
354 #[test]
355 fn nearest_returns_exact_pixel_value() {
356 let t = gradient_tile();
357 let v = sample(&t.bands[0], 4, 4, 2.5, 1.5, ResamplingMethod::Nearest).unwrap();
359 assert_eq!(v, 160.0);
360 }
361
362 #[test]
363 fn bilinear_interpolates_between_pixels() {
364 let t = gradient_tile();
365 let v = sample(&t.bands[0], 4, 4, 1.0, 0.5, ResamplingMethod::Bilinear).unwrap();
367 assert!((v - 40.0).abs() < 1e-9);
368 }
369
370 #[test]
371 fn cubic_handles_smooth_gradient() {
372 let t = gradient_tile();
373 let v = sample(&t.bands[0], 4, 4, 1.0, 0.5, ResamplingMethod::Cubic).unwrap();
377 assert!(v.is_finite());
378 assert!((20.0..=60.0).contains(&v), "cubic got {v}, expected 20..60");
379 }
380
381 #[test]
382 fn out_of_bounds_returns_none() {
383 let t = gradient_tile();
384 assert!(sample(&t.bands[0], 4, 4, -1.0, 0.0, ResamplingMethod::Nearest).is_none());
385 assert!(sample(&t.bands[0], 4, 4, 0.0, 5.0, ResamplingMethod::Nearest).is_none());
386 }
387
388 #[test]
389 fn resample_tile_upsamples() {
390 let t = gradient_tile();
391 let resized = resample_tile(&t, 8, 8, ResamplingMethod::Bilinear);
392 assert_eq!(resized.width, 8);
393 assert_eq!(resized.height, 8);
394 assert_eq!(resized.geo_transform.pixel_size, [0.5, -0.5]);
396 }
397
398 #[test]
399 fn resample_tile_downsamples_average() {
400 let t = gradient_tile();
403 let resized = resample_tile(&t, 2, 2, ResamplingMethod::Average);
404 assert_eq!(resized.width, 2);
405 assert_eq!(resized.height, 2);
406 let v = resized.bands[0].data[0];
409 assert!((v as i32 - 40).abs() < 2);
410 }
411
412 #[test]
413 fn resample_tile_downsamples_mode() {
414 let mut t = u8_tile(4, 4, 5);
416 t.bands[0].data[0] = 99;
417 let resized = resample_tile(&t, 2, 2, ResamplingMethod::Mode);
418 assert_eq!(resized.width, 2);
419 assert_eq!(resized.bands[0].data[0], 5);
421 }
422
423 #[test]
424 fn lanczos_kernel_at_zero_is_one() {
425 assert!((lanczos_kernel(0.0, 3) - 1.0).abs() < 1e-12);
426 assert_eq!(lanczos_kernel(3.0, 3), 0.0);
428 assert_eq!(lanczos_kernel(-3.0, 3), 0.0);
429 }
430}