use crate::common::Confidence;
use crate::error::{SceneError, SceneResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SceneType {
Indoor,
Outdoor,
Day,
Night,
Landscape,
Portrait,
Urban,
Natural,
Water,
Sky,
Unknown,
}
impl SceneType {
#[must_use]
pub fn all() -> &'static [Self] {
&[
Self::Indoor,
Self::Outdoor,
Self::Day,
Self::Night,
Self::Landscape,
Self::Portrait,
Self::Urban,
Self::Natural,
Self::Water,
Self::Sky,
Self::Unknown,
]
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Indoor => "Indoor",
Self::Outdoor => "Outdoor",
Self::Day => "Day",
Self::Night => "Night",
Self::Landscape => "Landscape",
Self::Portrait => "Portrait",
Self::Urban => "Urban",
Self::Natural => "Natural",
Self::Water => "Water",
Self::Sky => "Sky",
Self::Unknown => "Unknown",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneClassification {
pub scene_type: SceneType,
pub confidence: Confidence,
pub scores: Vec<(SceneType, f32)>,
pub features: SceneFeatures,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneFeatures {
pub brightness: f32,
pub color_temperature: f32,
pub saturation: f32,
pub sky_ratio: f32,
pub vegetation_ratio: f32,
pub structure_ratio: f32,
pub horizon_position: Option<f32>,
}
impl Default for SceneFeatures {
fn default() -> Self {
Self {
brightness: 0.5,
color_temperature: 0.5,
saturation: 0.5,
sky_ratio: 0.0,
vegetation_ratio: 0.0,
structure_ratio: 0.0,
horizon_position: None,
}
}
}
#[derive(Debug, Clone)]
pub struct SceneConfig {
pub confidence_threshold: f32,
pub use_color_histogram: bool,
pub use_edge_detection: bool,
pub use_texture_analysis: bool,
pub temporal_smoothing: bool,
pub temporal_window: usize,
pub temporal_decay: f32,
}
impl Default for SceneConfig {
fn default() -> Self {
Self {
confidence_threshold: 0.5,
use_color_histogram: true,
use_edge_detection: true,
use_texture_analysis: true,
temporal_smoothing: false,
temporal_window: 8,
temporal_decay: 0.3,
}
}
}
#[derive(Debug, Clone)]
struct TemporalBuffer {
scores: Vec<Vec<f32>>,
capacity: usize,
}
impl TemporalBuffer {
fn new(capacity: usize, _num_types: usize) -> Self {
Self {
scores: Vec::with_capacity(capacity),
capacity,
}
}
fn push(&mut self, frame_scores: Vec<f32>) {
if self.scores.len() >= self.capacity {
self.scores.remove(0);
}
self.scores.push(frame_scores);
}
fn smooth(&self, decay: f32) -> Vec<f32> {
if self.scores.is_empty() {
return Vec::new();
}
let len = self.scores[0].len();
let mut smoothed = vec![0.0f32; len];
let n = self.scores.len();
let mut weight_sum = 0.0_f32;
for (i, frame_scores) in self.scores.iter().enumerate() {
let age = (n - 1 - i) as f32;
let weight = (-decay * age).exp();
for (j, s) in frame_scores.iter().enumerate() {
smoothed[j] += s * weight;
}
weight_sum += weight;
}
if weight_sum > 0.0 {
for v in &mut smoothed {
*v /= weight_sum;
}
}
smoothed
}
}
pub struct SceneClassifier {
config: SceneConfig,
temporal_buffer: Option<std::sync::Mutex<TemporalBuffer>>,
}
impl SceneClassifier {
#[must_use]
pub fn new() -> Self {
Self {
config: SceneConfig::default(),
temporal_buffer: None,
}
}
#[must_use]
pub fn with_config(config: SceneConfig) -> Self {
let temporal_buffer = if config.temporal_smoothing {
let buf = TemporalBuffer::new(config.temporal_window, SceneType::all().len());
Some(std::sync::Mutex::new(buf))
} else {
None
};
Self {
config,
temporal_buffer,
}
}
#[must_use]
pub fn with_temporal_smoothing(window: usize) -> Self {
let config = SceneConfig {
temporal_smoothing: true,
temporal_window: window,
..SceneConfig::default()
};
Self::with_config(config)
}
pub fn reset_temporal_buffer(&self) {
if let Some(ref buf) = self.temporal_buffer {
if let Ok(mut guard) = buf.lock() {
guard.scores.clear();
}
}
}
pub fn classify(
&self,
rgb_data: &[u8],
width: usize,
height: usize,
) -> SceneResult<SceneClassification> {
if rgb_data.len() != width * height * 3 {
return Err(SceneError::InvalidDimensions(format!(
"Expected {} bytes, got {}",
width * height * 3,
rgb_data.len()
)));
}
let features = self.extract_features(rgb_data, width, height)?;
let raw_scores: Vec<f32> = vec![
self.score_indoor(&features),
self.score_outdoor(&features),
self.score_day(&features),
self.score_night(&features),
self.score_landscape(&features),
self.score_portrait(&features),
self.score_urban(&features),
self.score_natural(&features),
self.score_water(&features),
self.score_sky(&features),
];
let final_scores = if let Some(ref buf_mutex) = self.temporal_buffer {
if let Ok(mut buf) = buf_mutex.lock() {
buf.push(raw_scores.clone());
buf.smooth(self.config.temporal_decay)
} else {
raw_scores.clone()
}
} else {
raw_scores.clone()
};
let scene_types = [
SceneType::Indoor,
SceneType::Outdoor,
SceneType::Day,
SceneType::Night,
SceneType::Landscape,
SceneType::Portrait,
SceneType::Urban,
SceneType::Natural,
SceneType::Water,
SceneType::Sky,
];
let scores: Vec<(SceneType, f32)> = scene_types
.iter()
.zip(final_scores.iter())
.map(|(&t, &s)| (t, s))
.collect();
let (scene_type, confidence) = scores
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map_or((SceneType::Unknown, 0.0), |(t, s)| (*t, *s));
Ok(SceneClassification {
scene_type,
confidence: Confidence::new(confidence),
scores,
features,
})
}
fn extract_features(
&self,
rgb_data: &[u8],
width: usize,
height: usize,
) -> SceneResult<SceneFeatures> {
let mut brightness_sum = 0.0;
let mut saturation_sum = 0.0;
let mut color_temp_sum = 0.0;
let pixel_count = width * height;
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) * 3;
let r = f32::from(rgb_data[idx]);
let g = f32::from(rgb_data[idx + 1]);
let b = f32::from(rgb_data[idx + 2]);
brightness_sum += 0.299 * r + 0.587 * g + 0.114 * b;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
if max > 0.0 {
saturation_sum += (max - min) / max;
}
color_temp_sum += (b - r) / 255.0;
}
}
let brightness = (brightness_sum / (pixel_count as f32 * 255.0)).clamp(0.0, 1.0);
let saturation = (saturation_sum / pixel_count as f32).clamp(0.0, 1.0);
let color_temperature = ((color_temp_sum / pixel_count as f32) + 1.0) / 2.0;
let (sky_ratio, vegetation_ratio, structure_ratio) =
self.detect_regions(rgb_data, width, height);
let horizon_position = self.detect_horizon(rgb_data, width, height);
Ok(SceneFeatures {
brightness,
color_temperature,
saturation,
sky_ratio,
vegetation_ratio,
structure_ratio,
horizon_position,
})
}
fn detect_regions(&self, rgb_data: &[u8], width: usize, height: usize) -> (f32, f32, f32) {
let mut sky_pixels = 0;
let mut vegetation_pixels = 0;
let mut structure_pixels = 0;
let pixel_count = width * height;
for i in (0..rgb_data.len()).step_by(3) {
let r = rgb_data[i];
let g = rgb_data[i + 1];
let b = rgb_data[i + 2];
if b > r && b > g && b > 128 {
sky_pixels += 1;
}
else if g > r && g > b && g > 64 {
vegetation_pixels += 1;
}
else {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
if max > 0 && (max - min) < 30 {
structure_pixels += 1;
}
}
}
(
sky_pixels as f32 / pixel_count as f32,
vegetation_pixels as f32 / pixel_count as f32,
structure_pixels as f32 / pixel_count as f32,
)
}
fn detect_horizon(&self, rgb_data: &[u8], width: usize, height: usize) -> Option<f32> {
let start_y = height / 3;
let end_y = (height * 2) / 3;
let mut max_edge = 0.0;
let mut horizon_y = None;
for y in start_y..end_y {
let mut edge_strength = 0.0;
for x in 1..width - 1 {
let _idx = (y * width + x) * 3;
let idx_above = ((y - 1) * width + x) * 3;
let idx_below = ((y + 1) * width + x) * 3;
for c in 0..3 {
let diff = (rgb_data[idx_below + c] as i32 - rgb_data[idx_above + c] as i32)
.unsigned_abs() as f32;
edge_strength += diff;
}
}
if edge_strength > max_edge {
max_edge = edge_strength;
horizon_y = Some(y);
}
}
horizon_y.map(|y| y as f32 / height as f32)
}
fn score_indoor(&self, features: &SceneFeatures) -> f32 {
let mut score = 0.0;
score += (1.0 - features.brightness) * 0.3;
score += (1.0 - features.sky_ratio) * 0.4;
score += features.structure_ratio * 0.3;
score.clamp(0.0, 1.0)
}
fn score_outdoor(&self, features: &SceneFeatures) -> f32 {
let mut score = 0.0;
score += features.brightness * 0.3;
score += features.sky_ratio * 0.4;
score += features.vegetation_ratio * 0.3;
score.clamp(0.0, 1.0)
}
fn score_day(&self, features: &SceneFeatures) -> f32 {
(features.brightness * 0.7 + features.saturation * 0.3).clamp(0.0, 1.0)
}
fn score_night(&self, features: &SceneFeatures) -> f32 {
(1.0 - features.brightness).clamp(0.0, 1.0)
}
fn score_landscape(&self, features: &SceneFeatures) -> f32 {
let mut score = 0.0;
if features.horizon_position.is_some() {
score += 0.5;
}
score += (features.sky_ratio + features.vegetation_ratio) * 0.5;
score.clamp(0.0, 1.0)
}
fn score_portrait(&self, features: &SceneFeatures) -> f32 {
let mut score = 1.0 - features.sky_ratio;
if let Some(horizon) = features.horizon_position {
if (0.33..=0.67).contains(&horizon) {
score *= 0.5;
}
}
score.clamp(0.0, 1.0)
}
fn score_urban(&self, features: &SceneFeatures) -> f32 {
let mut score = features.structure_ratio * 0.6;
score += (1.0 - features.vegetation_ratio) * 0.4;
score.clamp(0.0, 1.0)
}
fn score_natural(&self, features: &SceneFeatures) -> f32 {
(features.vegetation_ratio * 0.7 + features.saturation * 0.3).clamp(0.0, 1.0)
}
fn score_water(&self, features: &SceneFeatures) -> f32 {
let mut score = 0.0;
if features.color_temperature > 0.5 {
score += (features.color_temperature - 0.5) * 2.0 * 0.5;
}
if let Some(horizon) = features.horizon_position {
if (0.33..=0.67).contains(&horizon) {
score += 0.5;
}
}
score.clamp(0.0, 1.0)
}
fn score_sky(&self, features: &SceneFeatures) -> f32 {
features.sky_ratio
}
}
impl Default for SceneClassifier {
fn default() -> Self {
Self::new()
}
}
pub struct SmoothedSceneClassifier {
inner: SceneClassifier,
smoother: crate::classify::temporal_smooth::TemporalSmoother<SceneType>,
}
impl SmoothedSceneClassifier {
#[must_use]
pub fn new(window_size: usize) -> Self {
Self {
inner: SceneClassifier::new(),
smoother: crate::classify::temporal_smooth::TemporalSmoother::new(window_size),
}
}
#[must_use]
pub fn with_classifier(classifier: SceneClassifier, window_size: usize) -> Self {
Self {
inner: classifier,
smoother: crate::classify::temporal_smooth::TemporalSmoother::new(window_size),
}
}
pub fn classify(
&mut self,
rgb_data: &[u8],
width: usize,
height: usize,
) -> crate::error::SceneResult<SceneType> {
let classification = self.inner.classify(rgb_data, width, height)?;
self.smoother.push(classification.scene_type);
Ok(self
.smoother
.current_class()
.copied()
.unwrap_or(classification.scene_type))
}
pub fn classify_full(
&mut self,
rgb_data: &[u8],
width: usize,
height: usize,
) -> crate::error::SceneResult<(SceneClassification, SceneType)> {
let classification = self.inner.classify(rgb_data, width, height)?;
self.smoother.push(classification.scene_type);
let smoothed = self
.smoother
.current_class()
.copied()
.unwrap_or(classification.scene_type);
Ok((classification, smoothed))
}
pub fn reset(&mut self) {
self.smoother.clear();
}
#[must_use]
pub fn window_size(&self) -> usize {
self.smoother.window_size
}
#[must_use]
pub fn window_len(&self) -> usize {
self.smoother.len()
}
}
fn solid_image(width: usize, height: usize, r: u8, g: u8, b: u8) -> Vec<u8> {
let mut data = vec![0u8; width * height * 3];
for i in (0..data.len()).step_by(3) {
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
}
data
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scene_type_name() {
assert_eq!(SceneType::Indoor.name(), "Indoor");
assert_eq!(SceneType::Outdoor.name(), "Outdoor");
}
#[test]
fn test_scene_classifier() {
let classifier = SceneClassifier::new();
let width = 100;
let height = 100;
let mut rgb_data = vec![0u8; width * height * 3];
for i in (0..rgb_data.len()).step_by(3) {
rgb_data[i] = 100; rgb_data[i + 1] = 150; rgb_data[i + 2] = 255; }
let result = classifier.classify(&rgb_data, width, height);
assert!(result.is_ok());
let classification = result.expect("should succeed in test");
assert!(classification.confidence.value() > 0.0);
}
#[test]
fn test_invalid_dimensions() {
let classifier = SceneClassifier::new();
let rgb_data = vec![0u8; 100];
let result = classifier.classify(&rgb_data, 10, 10);
assert!(result.is_err());
}
#[test]
fn test_scene_features_default() {
let features = SceneFeatures::default();
assert!((features.brightness - 0.5).abs() < f32::EPSILON);
assert!((features.saturation - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_temporal_smoothing_reduces_flicker() {
let classifier = SceneClassifier::with_temporal_smoothing(5);
let w = 80;
let h = 80;
let sky_frame = solid_image(w, h, 80, 120, 220);
let dark_frame = solid_image(w, h, 20, 20, 20);
let r1 = classifier.classify(&sky_frame, w, h).expect("ok");
let r2 = classifier.classify(&dark_frame, w, h).expect("ok");
let r3 = classifier.classify(&sky_frame, w, h).expect("ok");
assert!(r1.confidence.value() >= 0.0);
assert!(r2.confidence.value() >= 0.0);
assert!(r3.confidence.value() >= 0.0);
}
#[test]
fn test_reset_temporal_buffer() {
let classifier = SceneClassifier::with_temporal_smoothing(4);
let w = 60;
let h = 60;
let frame = solid_image(w, h, 100, 150, 200);
let _ = classifier.classify(&frame, w, h).expect("ok");
classifier.reset_temporal_buffer();
let r = classifier.classify(&frame, w, h).expect("ok");
assert!(r.confidence.value() >= 0.0);
}
#[test]
fn test_temporal_buffer_smooth() {
let mut buf = TemporalBuffer::new(3, 3);
buf.push(vec![1.0, 0.0, 0.0]);
buf.push(vec![0.0, 1.0, 0.0]);
buf.push(vec![0.0, 0.0, 1.0]);
let smoothed = buf.smooth(0.3);
assert_eq!(smoothed.len(), 3);
assert!(smoothed.iter().all(|&v| v >= 0.0 && v <= 1.0));
}
#[test]
fn test_smoothed_classifier_returns_scene_type() {
let mut sc = SmoothedSceneClassifier::new(3);
let w = 40;
let h = 40;
let frame = solid_image(w, h, 80, 120, 220);
let result = sc.classify(&frame, w, h);
assert!(result.is_ok(), "should classify successfully");
}
#[test]
fn test_smoothed_classifier_invalid_dimensions() {
let mut sc = SmoothedSceneClassifier::new(3);
let bad_data = vec![0u8; 10];
let result = sc.classify(&bad_data, 10, 10);
assert!(result.is_err(), "wrong-size buffer should error");
}
#[test]
fn test_smoothed_classifier_mode_stabilizes_outlier() {
let mut sc = SmoothedSceneClassifier::new(5);
let w = 40;
let h = 40;
let sky = solid_image(w, h, 80, 120, 220);
let dark = solid_image(w, h, 10, 10, 10);
sc.classify(&sky, w, h).expect("ok");
sc.classify(&sky, w, h).expect("ok");
sc.classify(&sky, w, h).expect("ok");
let after_outlier = sc.classify(&dark, w, h).expect("ok");
let after_sky = sc.classify(&sky, w, h).expect("ok");
assert_eq!(
after_outlier, after_sky,
"mode should stabilise away from single outlier"
);
}
#[test]
fn test_smoothed_classifier_reset_clears_window() {
let mut sc = SmoothedSceneClassifier::new(4);
let w = 30;
let h = 30;
let frame = solid_image(w, h, 100, 150, 200);
sc.classify(&frame, w, h).expect("ok");
sc.classify(&frame, w, h).expect("ok");
assert_eq!(sc.window_len(), 2);
sc.reset();
assert_eq!(sc.window_len(), 0, "reset should clear window");
}
#[test]
fn test_smoothed_classifier_window_size_reported() {
let sc = SmoothedSceneClassifier::new(7);
assert_eq!(sc.window_size(), 7);
}
#[test]
fn test_smoothed_classifier_classify_full_returns_both() {
let mut sc = SmoothedSceneClassifier::new(3);
let w = 40;
let h = 40;
let frame = solid_image(w, h, 80, 120, 220);
let result = sc.classify_full(&frame, w, h);
assert!(result.is_ok(), "classify_full should succeed");
let (classification, smoothed_type) = result.expect("ok");
assert!(classification.confidence.value() >= 0.0);
assert_eq!(smoothed_type, classification.scene_type);
}
#[test]
fn test_smoothed_classifier_with_custom_inner() {
let inner = SceneClassifier::with_temporal_smoothing(3);
let mut sc = SmoothedSceneClassifier::with_classifier(inner, 4);
let w = 30;
let h = 30;
let frame = solid_image(w, h, 200, 100, 50);
let result = sc.classify(&frame, w, h);
assert!(result.is_ok());
}
#[test]
fn test_smoothed_classifier_single_frame_window_len_one() {
let mut sc = SmoothedSceneClassifier::new(5);
let w = 20;
let h = 20;
let frame = solid_image(w, h, 100, 200, 100);
sc.classify(&frame, w, h).expect("ok");
assert_eq!(sc.window_len(), 1);
}
#[test]
fn test_smoothed_classifier_consistent_scene_stays_stable() {
let mut sc = SmoothedSceneClassifier::new(4);
let w = 50;
let h = 50;
let frame = solid_image(w, h, 50, 160, 60); let mut last_type = None;
for _ in 0..6 {
let t = sc.classify(&frame, w, h).expect("ok");
if let Some(prev) = last_type {
assert_eq!(t, prev, "consistent scene should produce stable label");
}
last_type = Some(t);
}
}
}