use crate::common::Confidence;
use crate::error::{SceneError, SceneResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, 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,
}
impl Default for SceneConfig {
fn default() -> Self {
Self {
confidence_threshold: 0.5,
use_color_histogram: true,
use_edge_detection: true,
use_texture_analysis: true,
}
}
}
pub struct SceneClassifier {
config: SceneConfig,
}
impl SceneClassifier {
#[must_use]
pub fn new() -> Self {
Self {
config: SceneConfig::default(),
}
}
#[must_use]
pub fn with_config(config: SceneConfig) -> Self {
Self { config }
}
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 mut scores = Vec::new();
scores.push((SceneType::Indoor, self.score_indoor(&features)));
scores.push((SceneType::Outdoor, self.score_outdoor(&features)));
scores.push((SceneType::Day, self.score_day(&features)));
scores.push((SceneType::Night, self.score_night(&features)));
scores.push((SceneType::Landscape, self.score_landscape(&features)));
scores.push((SceneType::Portrait, self.score_portrait(&features)));
scores.push((SceneType::Urban, self.score_urban(&features)));
scores.push((SceneType::Natural, self.score_natural(&features)));
scores.push((SceneType::Water, self.score_water(&features)));
scores.push((SceneType::Sky, self.score_sky(&features)));
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()
}
}
#[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);
}
}