use geo::{Simplify, unary_union};
use geo_types::{Coord, LineString, MultiPolygon, Polygon};
use geojson::{Feature, FeatureCollection, Geometry, GeometryValue};
use oxigdal_core::buffer::RasterBuffer;
use serde_json::{Map, Value as JsonValue};
use std::collections::HashSet;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use tracing::debug;
use crate::detection::GeoDetection;
use crate::error::{PostprocessingError, Result};
use crate::segmentation::SegmentationMask;
pub fn apply_threshold(probabilities: &RasterBuffer, threshold: f32) -> Result<RasterBuffer> {
if !(0.0..=1.0).contains(&threshold) {
return Err(PostprocessingError::InvalidThreshold { value: threshold }.into());
}
let mut result = probabilities.clone();
for y in 0..probabilities.height() {
for x in 0..probabilities.width() {
let prob =
probabilities
.get_pixel(x, y)
.map_err(|e| PostprocessingError::ExportFailed {
reason: format!("Failed to get probability: {}", e),
})?;
let value = if prob >= threshold as f64 { 1.0 } else { 0.0 };
result
.set_pixel(x, y, value)
.map_err(|e| PostprocessingError::ExportFailed {
reason: format!("Failed to set value: {}", e),
})?;
}
}
Ok(result)
}
pub fn mask_to_polygons(mask: &RasterBuffer, min_area: f64) -> Result<Vec<Polygon>> {
debug!(
"Converting {}x{} mask to polygons",
mask.width(),
mask.height()
);
let mut polygons = Vec::new();
let width = mask.width();
let height = mask.height();
let mut visited = vec![vec![false; width as usize]; height as usize];
for y in 0..height {
for x in 0..width {
if visited[y as usize][x as usize] {
continue;
}
let value =
mask.get_pixel(x, y)
.map_err(|e| PostprocessingError::PolygonConversionFailed {
reason: format!("Failed to get pixel: {}", e),
})?;
if value > 0.0 {
let polygon = trace_contour(mask, x, y, &mut visited)?;
let area = calculate_polygon_area(&polygon);
if area >= min_area {
polygons.push(polygon);
}
}
}
}
debug!("Extracted {} polygons", polygons.len());
Ok(polygons)
}
fn trace_contour(
mask: &RasterBuffer,
start_x: u64,
start_y: u64,
visited: &mut [Vec<bool>],
) -> Result<Polygon> {
let mut component: HashSet<(i64, i64)> = HashSet::new();
let mut stack = vec![(start_x, start_y)];
while let Some((x, y)) = stack.pop() {
if x >= mask.width() || y >= mask.height() {
continue;
}
if visited[y as usize][x as usize] {
continue;
}
let value =
mask.get_pixel(x, y)
.map_err(|e| PostprocessingError::PolygonConversionFailed {
reason: format!("Failed to get pixel: {}", e),
})?;
if value > 0.0 {
visited[y as usize][x as usize] = true;
component.insert((x as i64, y as i64));
if x > 0 {
stack.push((x - 1, y));
}
if x + 1 < mask.width() {
stack.push((x + 1, y));
}
if y > 0 {
stack.push((x, y - 1));
}
if y + 1 < mask.height() {
stack.push((x, y + 1));
}
}
}
let coords = trace_component_boundary(&component, start_x as i64, start_y as i64);
Ok(Polygon::new(LineString::from(coords), vec![]))
}
const DIR_OFFSETS: [(i64, i64); 4] = [(1, 0), (0, 1), (-1, 0), (0, -1)];
fn trace_component_boundary(
component: &HashSet<(i64, i64)>,
start_x: i64,
start_y: i64,
) -> Vec<Coord> {
let fg = |px: i64, py: i64| component.contains(&(px, py));
let right_left = |x: i64, y: i64, dir: usize| -> ((i64, i64), (i64, i64)) {
match dir {
0 => ((x, y), (x, y - 1)), 1 => ((x - 1, y), (x, y)), 2 => ((x - 1, y - 1), (x - 1, y)), _ => ((x, y - 1), (x - 1, y - 1)), }
};
let valid_edge = |x: i64, y: i64, dir: usize| -> bool {
let (r, l) = right_left(x, y, dir);
fg(r.0, r.1) && !fg(l.0, l.1)
};
let choose = |x: i64, y: i64, incoming: usize| -> Option<usize> {
[
(incoming + 1) % 4, incoming, (incoming + 3) % 4, (incoming + 2) % 4, ]
.into_iter()
.find(|&turn| valid_edge(x, y, turn))
};
let initial_dir = 0usize; let mut coords: Vec<Coord> = Vec::new();
let mut x = start_x;
let mut y = start_y;
let mut dir = initial_dir;
let mut first = true;
let max_steps = component.len().saturating_mul(4).saturating_add(8);
for _ in 0..max_steps {
let next_dir = match choose(x, y, dir) {
Some(d) => d,
None => break, };
if !first && x == start_x && y == start_y && next_dir == initial_dir {
break;
}
if first || next_dir != dir {
coords.push(Coord {
x: x as f64,
y: y as f64,
});
}
let (dx, dy) = DIR_OFFSETS[next_dir];
x += dx;
y += dy;
dir = next_dir;
first = false;
}
if let Some(first_coord) = coords.first().copied() {
if coords.last() != Some(&first_coord) {
coords.push(first_coord);
}
}
coords
}
fn calculate_polygon_area(polygon: &Polygon) -> f64 {
let coords = polygon.exterior().coords().collect::<Vec<_>>();
if coords.len() < 3 {
return 0.0;
}
let mut area = 0.0;
for i in 0..coords.len() - 1 {
area += coords[i].x * coords[i + 1].y - coords[i + 1].x * coords[i].y;
}
(area / 2.0).abs()
}
pub fn export_detections_geojson<P: AsRef<Path>>(
detections: &[GeoDetection],
output_path: P,
) -> Result<()> {
debug!("Exporting {} detections to GeoJSON", detections.len());
let features: Vec<Feature> = detections.iter().map(detection_to_feature).collect();
let collection = FeatureCollection {
bbox: None,
features,
foreign_members: None,
};
let json = serde_json::to_string_pretty(&collection).map_err(|e| {
PostprocessingError::ExportFailed {
reason: format!("Failed to serialize GeoJSON: {}", e),
}
})?;
let mut file =
File::create(output_path.as_ref()).map_err(|e| PostprocessingError::ExportFailed {
reason: format!("Failed to create output file: {}", e),
})?;
file.write_all(json.as_bytes())
.map_err(|e| PostprocessingError::ExportFailed {
reason: format!("Failed to write GeoJSON: {}", e),
})?;
debug!("Successfully exported detections");
Ok(())
}
fn detection_to_feature(det: &GeoDetection) -> Feature {
let polygon = det.geo_bbox.to_polygon();
let mut properties = Map::new();
properties.insert(
"class_id".to_string(),
JsonValue::Number(det.detection.class_id.into()),
);
properties.insert(
"confidence".to_string(),
JsonValue::Number(
serde_json::Number::from_f64(det.detection.confidence as f64)
.unwrap_or_else(|| serde_json::Number::from(0)),
),
);
if let Some(ref label) = det.detection.class_label {
properties.insert("class_label".to_string(), JsonValue::String(label.clone()));
}
for (key, value) in &det.detection.attributes {
properties.insert(key.clone(), JsonValue::String(value.clone()));
}
Feature {
bbox: None,
geometry: Some(Geometry::new(GeometryValue::from(&polygon))),
id: None,
properties: Some(properties),
foreign_members: None,
}
}
pub fn export_segmentation_geojson<P: AsRef<Path>>(
mask: &SegmentationMask,
output_path: P,
min_area: f64,
) -> Result<()> {
debug!("Exporting segmentation mask to GeoJSON");
let polygons = mask_to_polygons(&mask.mask, min_area)?;
let features: Vec<Feature> = polygons
.iter()
.enumerate()
.map(|(i, poly)| {
let mut properties = Map::new();
properties.insert("id".to_string(), JsonValue::Number(i.into()));
Feature {
bbox: None,
geometry: Some(Geometry::new(GeometryValue::from(poly))),
id: None,
properties: Some(properties),
foreign_members: None,
}
})
.collect();
let collection = FeatureCollection {
bbox: None,
features,
foreign_members: None,
};
let json = serde_json::to_string_pretty(&collection).map_err(|e| {
PostprocessingError::ExportFailed {
reason: format!("Failed to serialize GeoJSON: {}", e),
}
})?;
let mut file =
File::create(output_path.as_ref()).map_err(|e| PostprocessingError::ExportFailed {
reason: format!("Failed to create output file: {}", e),
})?;
file.write_all(json.as_bytes())
.map_err(|e| PostprocessingError::ExportFailed {
reason: format!("Failed to write GeoJSON: {}", e),
})?;
debug!("Successfully exported segmentation");
Ok(())
}
pub fn simplify_polygons(polygons: &[Polygon], tolerance: f64) -> Result<Vec<Polygon>> {
if tolerance < 0.0 {
return Err(PostprocessingError::ExportFailed {
reason: "Tolerance must be non-negative".to_string(),
}
.into());
}
Ok(polygons.iter().map(|p| p.simplify(tolerance)).collect())
}
pub fn merge_polygons(polygons: &[Polygon]) -> Result<MultiPolygon> {
if polygons.is_empty() {
return Ok(MultiPolygon::new(Vec::new()));
}
Ok(unary_union(polygons.iter()))
}
#[cfg(test)]
mod tests {
use super::*;
use oxigdal_core::types::RasterDataType;
use std::collections::HashMap;
#[test]
fn test_apply_threshold() {
let probs = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
let result = apply_threshold(&probs, 0.5);
assert!(result.is_ok());
}
#[test]
fn test_mask_to_polygons() {
let mut mask = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
let _ = mask.set_pixel(5, 5, 1.0);
let polygons = mask_to_polygons(&mask, 0.0);
assert!(polygons.is_ok());
}
#[test]
fn test_mask_to_polygons_square_outline() {
let mut mask = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 2..5 {
for x in 2..5 {
let _ = mask.set_pixel(x, y, 1.0);
}
}
let polygons = mask_to_polygons(&mask, 0.0).expect("trace square");
assert_eq!(polygons.len(), 1);
let poly = polygons.first().expect("one polygon");
assert_eq!(poly.exterior().0.len(), 5);
assert!((calculate_polygon_area(poly) - 9.0).abs() < 1e-9);
}
#[test]
fn test_mask_to_polygons_l_shape_not_bounding_box() {
let mut mask = RasterBuffer::zeros(8, 8, RasterDataType::Float32);
let _ = mask.set_pixel(1, 1, 1.0);
let _ = mask.set_pixel(1, 2, 1.0);
let _ = mask.set_pixel(1, 3, 1.0);
let _ = mask.set_pixel(2, 3, 1.0);
let _ = mask.set_pixel(3, 3, 1.0);
let polygons = mask_to_polygons(&mask, 0.0).expect("trace L");
assert_eq!(polygons.len(), 1);
let poly = polygons.first().expect("one polygon");
let area = calculate_polygon_area(poly);
assert!((area - 5.0).abs() < 1e-9, "expected L area 5, got {}", area);
assert!(
poly.exterior().0.len() > 5,
"L outline collapsed to a rectangle: {} coords",
poly.exterior().0.len()
);
}
#[test]
fn test_simplify_polygons_reduces_vertices() {
let poly = Polygon::new(
LineString::from(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 5.0, y: 0.0 }, Coord { x: 10.0, y: 0.0 },
Coord { x: 10.0, y: 10.0 },
Coord { x: 0.0, y: 10.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
);
let before = poly.exterior().0.len();
let simplified = simplify_polygons(&[poly], 0.5).expect("simplify");
let after = simplified.first().expect("one polygon").exterior().0.len();
assert!(
after < before,
"expected fewer vertices after simplify: {} -> {}",
before,
after
);
assert!(simplify_polygons(&[], -1.0).is_err());
}
#[test]
fn test_merge_polygons_unions_overlap() {
let a = Polygon::new(
LineString::from(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 2.0, y: 2.0 },
Coord { x: 0.0, y: 2.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
);
let b = Polygon::new(
LineString::from(vec![
Coord { x: 1.0, y: 1.0 },
Coord { x: 3.0, y: 1.0 },
Coord { x: 3.0, y: 3.0 },
Coord { x: 1.0, y: 3.0 },
Coord { x: 1.0, y: 1.0 },
]),
vec![],
);
let merged = merge_polygons(&[a, b]).expect("merge");
assert_eq!(merged.0.len(), 1);
let c = Polygon::new(
LineString::from(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: 0.0, y: 1.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
);
let d = Polygon::new(
LineString::from(vec![
Coord { x: 5.0, y: 5.0 },
Coord { x: 6.0, y: 5.0 },
Coord { x: 6.0, y: 6.0 },
Coord { x: 5.0, y: 6.0 },
Coord { x: 5.0, y: 5.0 },
]),
vec![],
);
let disjoint = merge_polygons(&[c, d]).expect("merge disjoint");
assert_eq!(disjoint.0.len(), 2);
}
#[test]
fn test_calculate_polygon_area() {
let polygon = Polygon::new(
LineString::from(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 10.0, y: 0.0 },
Coord { x: 10.0, y: 10.0 },
Coord { x: 0.0, y: 10.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
);
let area = calculate_polygon_area(&polygon);
assert!((area - 100.0).abs() < 1.0);
}
#[test]
fn test_export_detections_geojson() {
use crate::detection::{BoundingBox, Detection, GeoBoundingBox};
use std::env;
let temp_dir = env::temp_dir();
let output_path = temp_dir.join("test_detections.geojson");
let detections = vec![GeoDetection {
detection: Detection {
bbox: BoundingBox::new(0.0, 0.0, 10.0, 10.0),
class_id: 0,
class_label: Some("test".to_string()),
confidence: 0.9,
attributes: HashMap::new(),
},
geo_bbox: GeoBoundingBox {
min_x: 0.0,
min_y: 0.0,
max_x: 10.0,
max_y: 10.0,
},
}];
let result = export_detections_geojson(&detections, &output_path);
assert!(result.is_ok());
let _ = std::fs::remove_file(output_path);
}
}