use crate::vision::{RegionKind, RegionSet, VisionError};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct OverlayColorKey {
pub kind: RegionKind,
pub color: String,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct OverlayRequest {
pub output_path: PathBuf,
pub include_points: bool,
pub include_labels: bool,
}
impl fmt::Debug for OverlayRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OverlayRequest")
.field("output_path", &"[REDACTED]")
.field("include_points", &self.include_points)
.field("include_labels", &self.include_labels)
.finish()
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct OverlayArtifactSummary {
pub output_path_redacted: String,
pub dimensions: (u32, u32),
pub region_count: usize,
pub color_key: Vec<OverlayColorKey>,
}
impl fmt::Debug for OverlayArtifactSummary {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OverlayArtifactSummary")
.field("output_path", &"[REDACTED]")
.field("dimensions", &self.dimensions)
.field("region_count", &self.region_count)
.field("color_key", &self.color_key)
.finish()
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct VisionDebugOverlay {
pub summary: OverlayArtifactSummary,
pub warnings: Vec<String>,
}
impl fmt::Debug for VisionDebugOverlay {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VisionDebugOverlay")
.field("summary", &self.summary)
.field("warning_count", &self.warnings.len())
.finish()
}
}
pub struct SummaryOverlayRenderer;
impl crate::vision::DebugOverlayRenderer for SummaryOverlayRenderer {
fn render_overlay(
&self,
image: &crate::quality::NormalizedImage,
regions: &RegionSet,
_request: &OverlayRequest,
) -> Result<OverlayArtifactSummary, VisionError> {
Ok(OverlayArtifactSummary {
output_path_redacted: "[REDACTED]".to_string(),
dimensions: (image.width, image.height),
region_count: regions.regions.len(),
color_key: default_color_key(),
})
}
}
pub fn default_color_key() -> Vec<OverlayColorKey> {
vec![
OverlayColorKey {
kind: RegionKind::Skin,
color: "red".to_string(),
},
OverlayColorKey {
kind: RegionKind::Brow,
color: "brown".to_string(),
},
OverlayColorKey {
kind: RegionKind::Iris,
color: "blue".to_string(),
},
OverlayColorKey {
kind: RegionKind::Sclera,
color: "cyan".to_string(),
},
OverlayColorKey {
kind: RegionKind::Lip,
color: "magenta".to_string(),
},
OverlayColorKey {
kind: RegionKind::Hair,
color: "purple".to_string(),
},
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quality::NormalizedImage;
use crate::types::MeasurementMode;
use crate::vision::{DebugOverlayRenderer, ExtractionQualityReport};
#[test]
fn default_color_key_includes_only_drawn_product_regions() {
let kinds: Vec<_> = default_color_key()
.into_iter()
.map(|entry| entry.kind)
.collect();
assert_eq!(
kinds,
vec![
RegionKind::Skin,
RegionKind::Brow,
RegionKind::Iris,
RegionKind::Sclera,
RegionKind::Lip,
RegionKind::Hair,
]
);
}
#[test]
fn default_color_key_does_not_advertise_beard_or_orange() {
let color_key = default_color_key();
assert!(
!color_key
.iter()
.any(|entry| entry.kind == RegionKind::Beard)
);
assert!(!color_key.iter().any(|entry| entry.color == "orange"));
}
#[test]
fn summary_renderer_uses_default_product_color_key() {
let image = NormalizedImage {
width: 4,
height: 4,
pixels: Vec::new(),
measurement_mode: MeasurementMode::SrgbAssumed,
orientation_applied: true,
icc_status: "not_embedded".to_string(),
};
let regions = RegionSet {
image_width: 4,
image_height: 4,
regions: Vec::new(),
extraction_quality: ExtractionQualityReport {
faces_detected: 1,
selected_face_index: Some(0),
backend: "summary".to_string(),
warnings: Vec::new(),
},
warnings: Vec::new(),
};
let request = OverlayRequest {
output_path: "overlay.png".into(),
include_points: false,
include_labels: false,
};
let summary = SummaryOverlayRenderer
.render_overlay(&image, ®ions, &request)
.expect("summary overlay rendering should be infallible");
assert_eq!(summary.color_key, default_color_key());
assert!(
!summary
.color_key
.iter()
.any(|entry| entry.kind == RegionKind::Beard)
);
}
}