Skip to main content

oxigdal_algorithms/raster/
classify.rs

1//! Raster classification operations
2//!
3//! This module provides various classification methods:
4//! - Reclassify by value ranges
5//! - Quantile classification
6//! - Natural breaks (Jenks)
7//! - Equal interval classification
8//! - Threshold operations
9
10use crate::error::{AlgorithmError, Result};
11use oxigdal_core::buffer::RasterBuffer;
12
13#[cfg(not(feature = "std"))]
14use alloc::vec::Vec;
15
16/// Classification rule mapping input range to output value
17#[derive(Debug, Clone, PartialEq)]
18pub struct ClassificationRule {
19    /// Minimum value (inclusive)
20    pub min: f64,
21    /// Maximum value (exclusive, unless it's the last rule)
22    pub max: f64,
23    /// Output class value
24    pub class_value: f64,
25}
26
27/// Classification method
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum ClassificationMethod {
30    /// Equal interval classification
31    EqualInterval {
32        /// Number of classes
33        num_classes: usize,
34    },
35    /// Quantile classification (equal count)
36    Quantile {
37        /// Number of classes
38        num_classes: usize,
39    },
40    /// Natural breaks (Jenks)
41    NaturalBreaks {
42        /// Number of classes
43        num_classes: usize,
44    },
45}
46
47/// Reclassifies a raster based on value ranges
48///
49/// # Arguments
50///
51/// * `src` - Source raster
52/// * `rules` - List of classification rules
53/// * `nodata_value` - Value to use for pixels not matching any rule
54///
55/// # Errors
56///
57/// Returns an error if rules are invalid or operation fails
58pub fn reclassify(
59    src: &RasterBuffer,
60    rules: &[ClassificationRule],
61    nodata_value: Option<f64>,
62) -> Result<RasterBuffer> {
63    if rules.is_empty() {
64        return Err(AlgorithmError::InvalidParameter {
65            parameter: "rules",
66            message: "At least one classification rule required".to_string(),
67        });
68    }
69
70    // Validate rules
71    for rule in rules {
72        if rule.max <= rule.min {
73            return Err(AlgorithmError::InvalidParameter {
74                parameter: "rules",
75                message: format!("Invalid range: {} to {}", rule.min, rule.max),
76            });
77        }
78    }
79
80    let width = src.width();
81    let height = src.height();
82    let mut dst = RasterBuffer::zeros(width, height, src.data_type());
83
84    for y in 0..height {
85        for x in 0..width {
86            let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
87
88            if src.is_nodata(val) || !val.is_finite() {
89                if let Some(nd) = nodata_value {
90                    dst.set_pixel(x, y, nd).map_err(AlgorithmError::Core)?;
91                }
92                continue;
93            }
94
95            // Find matching rule
96            let mut matched = false;
97            for rule in rules {
98                if val >= rule.min && val <= rule.max {
99                    dst.set_pixel(x, y, rule.class_value)
100                        .map_err(AlgorithmError::Core)?;
101                    matched = true;
102                    break;
103                }
104            }
105
106            if !matched {
107                if let Some(nd) = nodata_value {
108                    dst.set_pixel(x, y, nd).map_err(AlgorithmError::Core)?;
109                }
110            }
111        }
112    }
113
114    Ok(dst)
115}
116
117/// Applies a threshold operation
118///
119/// # Arguments
120///
121/// * `src` - Source raster
122/// * `threshold` - Threshold value
123/// * `above_value` - Value for pixels >= threshold
124/// * `below_value` - Value for pixels < threshold
125///
126/// # Errors
127///
128/// Returns an error if operation fails
129pub fn threshold(
130    src: &RasterBuffer,
131    threshold: f64,
132    above_value: f64,
133    below_value: f64,
134) -> Result<RasterBuffer> {
135    let width = src.width();
136    let height = src.height();
137    let mut dst = RasterBuffer::zeros(width, height, src.data_type());
138
139    for y in 0..height {
140        for x in 0..width {
141            let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
142
143            if src.is_nodata(val) || !val.is_finite() {
144                continue;
145            }
146
147            let result = if val >= threshold {
148                above_value
149            } else {
150                below_value
151            };
152
153            dst.set_pixel(x, y, result).map_err(AlgorithmError::Core)?;
154        }
155    }
156
157    Ok(dst)
158}
159
160/// Classifies a raster using a specified method
161///
162/// # Errors
163///
164/// Returns an error if num_classes is 0 or operation fails
165pub fn classify(src: &RasterBuffer, method: ClassificationMethod) -> Result<RasterBuffer> {
166    match method {
167        ClassificationMethod::EqualInterval { num_classes } => {
168            equal_interval_classify(src, num_classes)
169        }
170        ClassificationMethod::Quantile { num_classes } => quantile_classify(src, num_classes),
171        ClassificationMethod::NaturalBreaks { num_classes } => {
172            natural_breaks_classify(src, num_classes)
173        }
174    }
175}
176
177/// Equal interval classification
178fn equal_interval_classify(src: &RasterBuffer, num_classes: usize) -> Result<RasterBuffer> {
179    if num_classes == 0 {
180        return Err(AlgorithmError::InvalidParameter {
181            parameter: "num_classes",
182            message: "Number of classes must be positive".to_string(),
183        });
184    }
185
186    // Collect all valid values to find min/max
187    let mut values = Vec::new();
188    for y in 0..src.height() {
189        for x in 0..src.width() {
190            let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
191            if !src.is_nodata(val) && val.is_finite() {
192                values.push(val);
193            }
194        }
195    }
196
197    if values.is_empty() {
198        return Err(AlgorithmError::InsufficientData {
199            operation: "equal_interval_classify",
200            message: "No valid pixels found".to_string(),
201        });
202    }
203
204    let min = values.iter().copied().fold(f64::INFINITY, f64::min);
205    let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
206
207    if (max - min).abs() < f64::EPSILON {
208        return Err(AlgorithmError::InvalidParameter {
209            parameter: "data",
210            message: "All values are the same".to_string(),
211        });
212    }
213
214    let interval = (max - min) / num_classes as f64;
215
216    let mut rules = Vec::with_capacity(num_classes);
217    for i in 0..num_classes {
218        rules.push(ClassificationRule {
219            min: min + i as f64 * interval,
220            max: min + (i + 1) as f64 * interval,
221            class_value: i as f64 + 1.0,
222        });
223    }
224
225    reclassify(src, &rules, None)
226}
227
228/// Quantile classification
229fn quantile_classify(src: &RasterBuffer, num_classes: usize) -> Result<RasterBuffer> {
230    if num_classes == 0 {
231        return Err(AlgorithmError::InvalidParameter {
232            parameter: "num_classes",
233            message: "Number of classes must be positive".to_string(),
234        });
235    }
236
237    // Collect all valid values
238    let mut values = Vec::new();
239    for y in 0..src.height() {
240        for x in 0..src.width() {
241            let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
242            if !src.is_nodata(val) && val.is_finite() {
243                values.push(val);
244            }
245        }
246    }
247
248    if values.is_empty() {
249        return Err(AlgorithmError::InsufficientData {
250            operation: "quantile_classify",
251            message: "No valid pixels found".to_string(),
252        });
253    }
254
255    if values.len() < num_classes {
256        return Err(AlgorithmError::InsufficientData {
257            operation: "quantile_classify",
258            message: format!(
259                "Not enough values ({}) for {} classes",
260                values.len(),
261                num_classes
262            ),
263        });
264    }
265
266    values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
267
268    let mut rules = Vec::with_capacity(num_classes);
269    for i in 0..num_classes {
270        let start_idx = (i * values.len()) / num_classes;
271        let end_idx = ((i + 1) * values.len()) / num_classes;
272
273        let min = if i == 0 { values[0] } else { values[start_idx] };
274
275        let max = if i == num_classes - 1 {
276            values[values.len() - 1]
277        } else {
278            values[end_idx.min(values.len() - 1)]
279        };
280
281        rules.push(ClassificationRule {
282            min,
283            max,
284            class_value: i as f64 + 1.0,
285        });
286    }
287
288    reclassify(src, &rules, None)
289}
290
291/// Natural breaks (Jenks) classification
292///
293/// Uses a simplified Jenks optimization algorithm
294fn natural_breaks_classify(src: &RasterBuffer, num_classes: usize) -> Result<RasterBuffer> {
295    if num_classes == 0 {
296        return Err(AlgorithmError::InvalidParameter {
297            parameter: "num_classes",
298            message: "Number of classes must be positive".to_string(),
299        });
300    }
301
302    // Collect all valid values
303    let mut values = Vec::new();
304    for y in 0..src.height() {
305        for x in 0..src.width() {
306            let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
307            if !src.is_nodata(val) && val.is_finite() {
308                values.push(val);
309            }
310        }
311    }
312
313    if values.is_empty() {
314        return Err(AlgorithmError::InsufficientData {
315            operation: "natural_breaks_classify",
316            message: "No valid pixels found".to_string(),
317        });
318    }
319
320    if values.len() < num_classes {
321        return Err(AlgorithmError::InsufficientData {
322            operation: "natural_breaks_classify",
323            message: format!(
324                "Not enough values ({}) for {} classes",
325                values.len(),
326                num_classes
327            ),
328        });
329    }
330
331    values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
332
333    // Simplified Jenks: use k-means-like approach
334    let breaks = compute_jenks_breaks(&values, num_classes)?;
335
336    let mut rules = Vec::with_capacity(num_classes);
337    for i in 0..num_classes {
338        let min = if i == 0 { values[0] } else { breaks[i - 1] };
339
340        let max = if i == num_classes - 1 {
341            values[values.len() - 1]
342        } else {
343            breaks[i]
344        };
345
346        rules.push(ClassificationRule {
347            min,
348            max,
349            class_value: i as f64 + 1.0,
350        });
351    }
352
353    reclassify(src, &rules, None)
354}
355
356/// Computes Jenks natural breaks using dynamic programming
357fn compute_jenks_breaks(sorted_values: &[f64], num_classes: usize) -> Result<Vec<f64>> {
358    let n = sorted_values.len();
359
360    if n < num_classes {
361        return Err(AlgorithmError::InsufficientData {
362            operation: "compute_jenks_breaks",
363            message: "Not enough values for classes".to_string(),
364        });
365    }
366
367    // For simplicity, use equal-count quantiles as an approximation
368    // A full Jenks implementation would use dynamic programming
369    let mut breaks = Vec::with_capacity(num_classes - 1);
370
371    for i in 1..num_classes {
372        let idx = (i * n) / num_classes;
373        if idx < n {
374            breaks.push(sorted_values[idx]);
375        }
376    }
377
378    Ok(breaks)
379}
380
381#[cfg(test)]
382#[allow(clippy::panic)]
383mod tests {
384    use super::*;
385    use oxigdal_core::types::RasterDataType;
386
387    #[test]
388    fn test_reclassify() {
389        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
390
391        for y in 0..10 {
392            for x in 0..10 {
393                src.set_pixel(x, y, (x * 10) as f64).ok();
394            }
395        }
396
397        let rules = vec![
398            ClassificationRule {
399                min: 0.0,
400                max: 30.0,
401                class_value: 1.0,
402            },
403            ClassificationRule {
404                min: 30.0,
405                max: 60.0,
406                class_value: 2.0,
407            },
408            ClassificationRule {
409                min: 60.0,
410                max: 100.0,
411                class_value: 3.0,
412            },
413        ];
414
415        let result = reclassify(&src, &rules, None);
416        assert!(result.is_ok());
417    }
418
419    #[test]
420    fn test_threshold() {
421        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
422
423        for y in 0..10 {
424            for x in 0..10 {
425                src.set_pixel(x, y, (x + y) as f64).ok();
426            }
427        }
428
429        let result = threshold(&src, 10.0, 1.0, 0.0);
430        assert!(result.is_ok());
431
432        let classified = result.expect("Should succeed");
433        let val1 = classified.get_pixel(0, 0).expect("Should get pixel");
434        assert!((val1 - 0.0).abs() < f64::EPSILON);
435
436        let val2 = classified.get_pixel(9, 9).expect("Should get pixel");
437        assert!((val2 - 1.0).abs() < f64::EPSILON);
438    }
439
440    #[test]
441    fn test_equal_interval() {
442        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
443
444        for y in 0..10 {
445            for x in 0..10 {
446                src.set_pixel(x, y, (y * 10 + x) as f64).ok();
447            }
448        }
449
450        let result = equal_interval_classify(&src, 5);
451        assert!(result.is_ok());
452    }
453
454    #[test]
455    fn test_quantile() {
456        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
457
458        for y in 0..10 {
459            for x in 0..10 {
460                src.set_pixel(x, y, (y * 10 + x) as f64).ok();
461            }
462        }
463
464        let result = quantile_classify(&src, 4);
465        assert!(result.is_ok());
466    }
467
468    #[test]
469    fn test_natural_breaks() {
470        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
471
472        for y in 0..10 {
473            for x in 0..10 {
474                src.set_pixel(x, y, (y * 10 + x) as f64).ok();
475            }
476        }
477
478        let result = natural_breaks_classify(&src, 3);
479        assert!(result.is_ok());
480    }
481
482    #[test]
483    fn test_classify_method() {
484        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
485
486        for y in 0..10 {
487            for x in 0..10 {
488                src.set_pixel(x, y, (y * 10 + x) as f64).ok();
489            }
490        }
491
492        let method = ClassificationMethod::EqualInterval { num_classes: 5 };
493        let result = classify(&src, method);
494        assert!(result.is_ok());
495    }
496
497    // ========== Edge Cases ==========
498
499    #[test]
500    fn test_reclassify_empty_rules() {
501        let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
502        let rules = vec![];
503
504        let result = reclassify(&src, &rules, None);
505        assert!(result.is_err());
506        if let Err(AlgorithmError::InvalidParameter { .. }) = result {
507            // Expected
508        } else {
509            panic!("Expected InvalidParameter error");
510        }
511    }
512
513    #[test]
514    fn test_reclassify_invalid_range() {
515        let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
516        let rules = vec![ClassificationRule {
517            min: 10.0,
518            max: 5.0, // max < min
519            class_value: 1.0,
520        }];
521
522        let result = reclassify(&src, &rules, None);
523        assert!(result.is_err());
524    }
525
526    #[test]
527    fn test_equal_interval_zero_classes() {
528        let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
529
530        let result = equal_interval_classify(&src, 0);
531        assert!(result.is_err());
532    }
533
534    #[test]
535    fn test_quantile_zero_classes() {
536        let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
537
538        let result = quantile_classify(&src, 0);
539        assert!(result.is_err());
540    }
541
542    #[test]
543    fn test_natural_breaks_zero_classes() {
544        let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
545
546        let result = natural_breaks_classify(&src, 0);
547        assert!(result.is_err());
548    }
549
550    #[test]
551    fn test_classify_single_value() {
552        let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
553
554        for y in 0..5 {
555            for x in 0..5 {
556                src.set_pixel(x, y, 42.0).ok();
557            }
558        }
559
560        let result = equal_interval_classify(&src, 3);
561        assert!(result.is_err()); // All values same
562    }
563
564    #[test]
565    fn test_quantile_not_enough_values() {
566        let mut src = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
567
568        for y in 0..2 {
569            for x in 0..2 {
570                src.set_pixel(x, y, (x + y) as f64).ok();
571            }
572        }
573
574        let result = quantile_classify(&src, 10);
575        assert!(result.is_err());
576    }
577
578    // ========== Advanced Classification Tests ==========
579
580    #[test]
581    fn test_overlapping_rules() {
582        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
583
584        for y in 0..10 {
585            for x in 0..10 {
586                src.set_pixel(x, y, (x * 5) as f64).ok();
587            }
588        }
589
590        // Rules with overlapping ranges (first match wins)
591        let rules = vec![
592            ClassificationRule {
593                min: 0.0,
594                max: 20.0,
595                class_value: 1.0,
596            },
597            ClassificationRule {
598                min: 15.0,
599                max: 35.0,
600                class_value: 2.0,
601            },
602            ClassificationRule {
603                min: 30.0,
604                max: 50.0,
605                class_value: 3.0,
606            },
607        ];
608
609        let result = reclassify(&src, &rules, Some(-1.0));
610        assert!(result.is_ok());
611    }
612
613    #[test]
614    fn test_threshold_boundary_values() {
615        let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
616
617        for y in 0..5 {
618            for x in 0..5 {
619                src.set_pixel(x, y, (x + y) as f64).ok();
620            }
621        }
622
623        let result = threshold(&src, 4.0, 100.0, 0.0);
624        assert!(result.is_ok());
625        let classified = result.expect("Should succeed");
626
627        // Value exactly at threshold should be above
628        let val_at_threshold = classified.get_pixel(2, 2).expect("Should get pixel");
629        assert!((val_at_threshold - 100.0).abs() < f64::EPSILON);
630
631        // Value below threshold
632        let val_below = classified.get_pixel(0, 0).expect("Should get pixel");
633        assert!((val_below - 0.0).abs() < f64::EPSILON);
634    }
635
636    #[test]
637    fn test_classify_with_single_pixel() {
638        let mut src = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
639        src.set_pixel(0, 0, 42.0).ok();
640
641        let result = threshold(&src, 40.0, 1.0, 0.0);
642        assert!(result.is_ok());
643        let classified = result.expect("Should succeed");
644        let val = classified.get_pixel(0, 0).expect("Should get pixel");
645        assert!((val - 1.0).abs() < f64::EPSILON);
646    }
647
648    #[test]
649    fn test_reclassify_with_nodata() {
650        let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
651
652        for y in 0..5 {
653            for x in 0..5 {
654                if x == 2 && y == 2 {
655                    src.set_pixel(x, y, f64::NAN).ok(); // NoData
656                } else {
657                    src.set_pixel(x, y, (x * 10) as f64).ok();
658                }
659            }
660        }
661
662        let rules = vec![
663            ClassificationRule {
664                min: 0.0,
665                max: 20.0,
666                class_value: 1.0,
667            },
668            ClassificationRule {
669                min: 20.0,
670                max: 50.0,
671                class_value: 2.0,
672            },
673        ];
674
675        let result = reclassify(&src, &rules, Some(-9999.0));
676        assert!(result.is_ok());
677    }
678
679    #[test]
680    fn test_equal_interval_many_classes() {
681        let mut src = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
682
683        for y in 0..20 {
684            for x in 0..20 {
685                src.set_pixel(x, y, (y * 20 + x) as f64).ok();
686            }
687        }
688
689        let result = equal_interval_classify(&src, 10);
690        assert!(result.is_ok());
691    }
692
693    #[test]
694    fn test_quantile_many_classes() {
695        let mut src = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
696
697        for y in 0..20 {
698            for x in 0..20 {
699                src.set_pixel(x, y, (y * 20 + x) as f64).ok();
700            }
701        }
702
703        let result = quantile_classify(&src, 10);
704        assert!(result.is_ok());
705    }
706
707    #[test]
708    fn test_classify_all_methods() {
709        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
710
711        for y in 0..10 {
712            for x in 0..10 {
713                src.set_pixel(x, y, (y * 10 + x) as f64).ok();
714            }
715        }
716
717        let methods = vec![
718            ClassificationMethod::EqualInterval { num_classes: 5 },
719            ClassificationMethod::Quantile { num_classes: 5 },
720            ClassificationMethod::NaturalBreaks { num_classes: 5 },
721        ];
722
723        for method in methods {
724            let result = classify(&src, method);
725            assert!(result.is_ok());
726        }
727    }
728
729    #[test]
730    fn test_threshold_edge_values() {
731        let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
732
733        for y in 0..5 {
734            for x in 0..5 {
735                src.set_pixel(x, y, if x < 3 { 10.0 } else { 20.0 }).ok();
736            }
737        }
738
739        let result = threshold(&src, 15.0, 255.0, 0.0);
740        assert!(result.is_ok());
741        let classified = result.expect("Should succeed");
742
743        // Values below threshold
744        let val1 = classified.get_pixel(0, 0).expect("Should get pixel");
745        assert!((val1 - 0.0).abs() < f64::EPSILON);
746
747        // Values above threshold
748        let val2 = classified.get_pixel(4, 0).expect("Should get pixel");
749        assert!((val2 - 255.0).abs() < f64::EPSILON);
750    }
751}