#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Genre {
Action,
Documentary,
Drama,
Comedy,
Horror,
News,
Sports,
Nature,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mood {
Happy,
Sad,
Tense,
Calm,
Exciting,
Dark,
Neutral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LocationTag {
Indoor,
Outdoor,
Urban,
Rural,
Studio,
Nature,
Underwater,
Aerial,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassificationResult {
pub genre: GenreScore,
pub mood: MoodScore,
pub location_tags: Vec<LocationScore>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenreScore {
pub genre: Genre,
pub confidence: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MoodScore {
pub mood: Mood,
pub confidence: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationScore {
pub tag: LocationTag,
pub confidence: f32,
}
#[derive(Debug, Clone)]
pub struct SceneFeatures {
pub mean_luminance: f32,
pub mean_saturation: f32,
pub dominant_hue: f32,
pub motion_magnitude: f32,
pub sky_ratio: f32,
pub skin_ratio: f32,
}
impl SceneFeatures {
#[must_use]
pub fn new(
mean_luminance: f32,
mean_saturation: f32,
dominant_hue: f32,
motion_magnitude: f32,
sky_ratio: f32,
skin_ratio: f32,
) -> Self {
Self {
mean_luminance,
mean_saturation,
dominant_hue,
motion_magnitude,
sky_ratio,
skin_ratio,
}
}
}
#[derive(Debug, Default)]
pub struct SceneClassifier {
threshold: f32,
}
impl SceneClassifier {
#[must_use]
pub fn new() -> Self {
Self { threshold: 0.3 }
}
#[must_use]
pub fn with_threshold(threshold: f32) -> Self {
Self {
threshold: threshold.clamp(0.0, 1.0),
}
}
#[must_use]
pub fn classify(&self, features: &SceneFeatures) -> ClassificationResult {
ClassificationResult {
genre: self.detect_genre(features),
mood: self.estimate_mood(features),
location_tags: self.tag_location(features),
}
}
fn detect_genre(&self, f: &SceneFeatures) -> GenreScore {
if f.motion_magnitude > 0.6 && f.mean_saturation > 150.0 {
return GenreScore {
genre: Genre::Action,
confidence: 0.7_f32.max(self.threshold),
};
}
if f.sky_ratio > 0.4 {
return GenreScore {
genre: Genre::Nature,
confidence: 0.65,
};
}
if f.mean_luminance < 60.0 && f.mean_saturation < 80.0 {
return GenreScore {
genre: Genre::Horror,
confidence: 0.6,
};
}
GenreScore {
genre: Genre::Unknown,
confidence: 0.4,
}
}
fn estimate_mood(&self, f: &SceneFeatures) -> MoodScore {
if f.mean_luminance > 180.0 && f.mean_saturation > 120.0 {
return MoodScore {
mood: Mood::Happy,
confidence: 0.75,
};
}
if f.mean_luminance < 80.0 {
return MoodScore {
mood: Mood::Dark,
confidence: 0.7,
};
}
if f.motion_magnitude > 0.7 {
return MoodScore {
mood: Mood::Exciting,
confidence: 0.65,
};
}
if f.sky_ratio > 0.3 && f.motion_magnitude < 0.2 {
return MoodScore {
mood: Mood::Calm,
confidence: 0.6,
};
}
MoodScore {
mood: Mood::Neutral,
confidence: 0.5,
}
}
fn tag_location(&self, f: &SceneFeatures) -> Vec<LocationScore> {
let mut tags: Vec<LocationScore> = Vec::new();
if f.sky_ratio > 0.2 {
tags.push(LocationScore {
tag: LocationTag::Outdoor,
confidence: (0.5 + f.sky_ratio * 0.5).min(1.0),
});
} else {
tags.push(LocationScore {
tag: LocationTag::Indoor,
confidence: 0.5 + (1.0 - f.sky_ratio) * 0.3,
});
}
if f.sky_ratio > 0.3 && f.skin_ratio < 0.1 {
tags.push(LocationScore {
tag: LocationTag::Nature,
confidence: 0.6,
});
}
if f.sky_ratio > 0.7 {
tags.push(LocationScore {
tag: LocationTag::Aerial,
confidence: 0.55,
});
}
if f.sky_ratio < 0.05 && f.skin_ratio > 0.15 {
tags.push(LocationScore {
tag: LocationTag::Studio,
confidence: 0.55,
});
}
tags.retain(|t| t.confidence >= self.threshold);
tags.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
tags
}
}
#[cfg(test)]
mod tests {
use super::*;
fn action_features() -> SceneFeatures {
SceneFeatures::new(140.0, 180.0, 30.0, 0.8, 0.1, 0.2)
}
fn dark_features() -> SceneFeatures {
SceneFeatures::new(50.0, 60.0, 200.0, 0.1, 0.05, 0.05)
}
fn nature_features() -> SceneFeatures {
SceneFeatures::new(160.0, 100.0, 120.0, 0.15, 0.55, 0.02)
}
fn bright_features() -> SceneFeatures {
SceneFeatures::new(200.0, 140.0, 60.0, 0.1, 0.1, 0.3)
}
#[test]
fn test_classifier_new() {
let c = SceneClassifier::new();
assert!((c.threshold - 0.3).abs() < f32::EPSILON);
}
#[test]
fn test_classifier_with_threshold() {
let c = SceneClassifier::with_threshold(0.5);
assert!((c.threshold - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_threshold_clamped_high() {
let c = SceneClassifier::with_threshold(2.0);
assert!((c.threshold - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_threshold_clamped_low() {
let c = SceneClassifier::with_threshold(-1.0);
assert!((c.threshold - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_genre_action_detected() {
let c = SceneClassifier::new();
let r = c.classify(&action_features());
assert_eq!(r.genre.genre, Genre::Action);
assert!(r.genre.confidence >= 0.3);
}
#[test]
fn test_genre_horror_detected() {
let c = SceneClassifier::new();
let r = c.classify(&dark_features());
assert_eq!(r.genre.genre, Genre::Horror);
}
#[test]
fn test_genre_nature_detected() {
let c = SceneClassifier::new();
let r = c.classify(&nature_features());
assert_eq!(r.genre.genre, Genre::Nature);
}
#[test]
fn test_mood_dark() {
let c = SceneClassifier::new();
let r = c.classify(&dark_features());
assert_eq!(r.mood.mood, Mood::Dark);
}
#[test]
fn test_mood_happy_bright() {
let c = SceneClassifier::new();
let r = c.classify(&bright_features());
assert_eq!(r.mood.mood, Mood::Happy);
}
#[test]
fn test_mood_exciting_action() {
let c = SceneClassifier::new();
let r = c.classify(&action_features());
assert_eq!(r.mood.mood, Mood::Exciting);
}
#[test]
fn test_location_tags_not_empty() {
let c = SceneClassifier::new();
let r = c.classify(&nature_features());
assert!(!r.location_tags.is_empty());
}
#[test]
fn test_location_outdoor_nature() {
let c = SceneClassifier::new();
let r = c.classify(&nature_features());
let has_outdoor = r
.location_tags
.iter()
.any(|t| t.tag == LocationTag::Outdoor);
assert!(has_outdoor);
}
#[test]
fn test_location_tags_sorted_by_confidence() {
let c = SceneClassifier::new();
let r = c.classify(&nature_features());
let confs: Vec<f32> = r.location_tags.iter().map(|t| t.confidence).collect();
for w in confs.windows(2) {
assert!(w[0] >= w[1]);
}
}
#[test]
fn test_scene_features_new() {
let f = SceneFeatures::new(100.0, 80.0, 180.0, 0.3, 0.2, 0.1);
assert!((f.mean_luminance - 100.0).abs() < f32::EPSILON);
assert!((f.dominant_hue - 180.0).abs() < f32::EPSILON);
}
#[test]
fn test_confidence_in_range() {
let c = SceneClassifier::new();
let r = c.classify(&action_features());
assert!(r.genre.confidence >= 0.0 && r.genre.confidence <= 1.0);
assert!(r.mood.confidence >= 0.0 && r.mood.confidence <= 1.0);
}
}