meteor_detect/demos/watermark_demo.rs

446 lines
17 KiB
Rust

use std::io;
use std::path::PathBuf;
use chrono::Utc;
use opencv::{core, imgcodecs, imgproc, highgui, prelude::*};
// Simplified watermark overlay module for demo
mod overlay {
use anyhow::Result;
use chrono::{DateTime, Utc};
use opencv::{core, imgproc, prelude::*};
use serde::{Deserialize, Serialize};
use freetype::{Library, Face};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WatermarkPosition {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Custom(u32, u32),
}
impl Default for WatermarkPosition {
fn default() -> Self {
Self::BottomLeft
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkOptions {
pub enabled: bool,
pub position: WatermarkPosition,
pub font_scale: f64,
pub thickness: i32,
pub color: (u8, u8, u8, u8),
pub background: bool,
pub background_color: (u8, u8, u8, u8),
pub padding: i32,
}
impl Default for WatermarkOptions {
fn default() -> Self {
Self {
enabled: true,
position: WatermarkPosition::BottomLeft,
font_scale: 0.6,
thickness: 1,
color: (255, 255, 255, 255), // White
background: true,
background_color: (0, 0, 0, 128), // Semi-transparent black
padding: 8,
}
}
}
pub struct WatermarkDemo {
options: WatermarkOptions,
gps_position: (f64, f64, f64), // lat, lon, alt
temperature: f32,
humidity: f32,
freetype_lib: Library,
font_face: Face, // Use a static lifetime for simplicity in demo
}
impl WatermarkDemo {
pub fn new(options: WatermarkOptions) -> Result<Self> {
let freetype_lib = Library::init()?;
// Dynamically set font_path based on OS using conditional compilation
let font_path = if cfg!(target_os = "windows") {
"C:\\Windows\\Fonts\\arial.ttf" // Common Windows font
} else if cfg!(target_os = "linux") {
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" // Common Linux font
} else if cfg!(target_os = "macos") {
"/System/Library/Fonts/Supplemental/Arial.ttf" // Common macOS font
} else {
// Fallback or error for other OS
eprintln!("Warning: Could not determine a suitable font path for the current OS. Using a default that might not exist.");
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" // Default fallback
};
let font_face = freetype_lib.new_face(font_path, 0)?;
Ok(Self {
options,
gps_position: (34.0522, -118.2437, 85.0), // Default to Los Angeles
temperature: 25.0,
humidity: 60.0,
freetype_lib,
font_face,
})
}
pub fn set_gps(&mut self, lat: f64, lon: f64, alt: f64) {
self.gps_position = (lat, lon, alt);
}
pub fn set_weather(&mut self, temp: f32, humidity: f32) {
self.temperature = temp;
self.humidity = humidity;
}
pub fn apply(&mut self, frame: &mut core::Mat, timestamp: DateTime<Utc>) -> Result<()> {
if !self.options.enabled {
return Ok(());
}
// Build text lines
let mut lines = Vec::new();
// Timestamp
lines.push(timestamp.format("%Y-%m-%d %H:%M:%S%.3f").to_string());
// GPS coordinates
let (lat, lon, alt) = self.gps_position;
lines.push(format!("Lat: {:.6}° Lon: {:.6}° Alt: {:.1}m", lat, lon, alt));
// Environment data
lines.push(format!(
"Temp: {:.1}°C Humidity: {:.1}%",
self.temperature, self.humidity
));
// Skip if no content
if lines.is_empty() {
return Ok(());
}
// Get frame dimensions
let width = frame.cols();
let height = frame.rows();
// Set font size based on options.font_scale (adjust as needed)
let font_size = (self.options.font_scale * 30.0) as isize; // Example scaling
self.font_face.set_pixel_sizes(0, font_size as u32)?;
let padding = self.options.padding;
let line_spacing = 4; // Space between lines
// Calculate text size and total block size using FreeType
let mut line_heights = Vec::with_capacity(lines.len());
let mut max_width = 0;
let mut total_height = 0;
for line in &lines {
let mut line_width = 0;
let mut line_height = 0;
for char in line.chars() {
self.font_face.load_char(char as usize, freetype::face::LoadFlag::RENDER)?;
let glyph = self.font_face.glyph();
let metrics = glyph.metrics();
line_width += (metrics.horiAdvance / 64) as i32;
line_height = line_height.max((metrics.height / 64) as i32);
}
line_heights.push(line_height);
max_width = max_width.max(line_width);
total_height += line_height;
}
let text_block_height = total_height + line_spacing * (lines.len() as i32 - 1) + padding * 2;
let text_block_width = max_width + padding * 2;
// Calculate watermark position
let (x, y) = match self.options.position {
WatermarkPosition::TopLeft => (padding, padding),
WatermarkPosition::TopRight => (width - text_block_width - padding, padding),
WatermarkPosition::BottomLeft => (padding, height - text_block_height - padding),
WatermarkPosition::BottomRight => (
width - text_block_width - padding,
height - text_block_height - padding
),
WatermarkPosition::Custom(x, y) => (x as i32, y as i32),
};
// Draw background rectangle if enabled
if self.options.background {
let bg_color = core::Scalar::new(
self.options.background_color.0 as f64,
self.options.background_color.1 as f64,
self.options.background_color.2 as f64,
self.options.background_color.3 as f64,
);
let rect = core::Rect::new(
x,
y,
text_block_width,
text_block_height
);
imgproc::rectangle(
frame,
rect,
bg_color,
-1, // Fill
imgproc::LINE_8,
0,
)?;
}
// Draw text lines using FreeType
let text_color = core::Scalar::new(
self.options.color.0 as f64,
self.options.color.1 as f64,
self.options.color.2 as f64,
self.options.color.3 as f64,
);
let mut current_y = y + padding;
for (i, line) in lines.iter().enumerate() {
let mut current_x = x + padding;
for char in line.chars() {
self.font_face.load_char(char as usize, freetype::face::LoadFlag::RENDER)?;
let glyph = self.font_face.glyph();
let bitmap = glyph.bitmap();
let metrics = glyph.metrics();
let bitmap_width = bitmap.width() as usize;
let bitmap_rows = bitmap.rows() as usize;
let bitmap_buffer = bitmap.buffer();
// Calculate text position for this character
let char_x = current_x + (metrics.horiBearingX / 64) as i32;
let char_y = current_y + (line_heights[i] as f64 * 0.8) as i32 - (metrics.horiBearingY / 64) as i32; // Adjust baseline
// Draw the glyph bitmap onto the frame
for row in 0..bitmap_rows {
for col in 0..bitmap_width {
let bitmap_pixel = bitmap_buffer[row * bitmap.pitch() as usize + col];
if bitmap_pixel > 0 {
let frame_x = char_x + col as i32;
let frame_y = char_y + row as i32;
// Ensure pixel is within frame bounds
if frame_x >= 0 && frame_x < width && frame_y >= 0 && frame_y < height {
let mut pixel = frame.at_2d_mut::<core::Vec3b>(frame_y, frame_x)?;
pixel[0] = text_color[0] as u8;
pixel[1] = text_color[1] as u8;
pixel[2] = text_color[2] as u8;
}
}
}
}
current_x += (metrics.horiAdvance / 64) as i32;
}
current_y += line_heights[i] + line_spacing;
}
Ok(())
}
}
}
// Simplified camera module for demo
mod camera {
use opencv::{core, prelude::*, videoio};
use chrono::{DateTime, Utc};
pub struct Frame {
pub image: core::Mat,
pub timestamp: DateTime<Utc>,
pub index: u64,
}
impl Frame {
pub fn new(image: core::Mat, timestamp: DateTime<Utc>, index: u64) -> Self {
Self { image, timestamp, index }
}
}
pub struct CameraDemo {
capture: videoio::VideoCapture,
frame_count: u64,
}
impl CameraDemo {
pub fn new(device_id: i32) -> Result<Self, Box<dyn std::error::Error>> {
let capture = videoio::VideoCapture::new(device_id, videoio::CAP_ANY)?;
if !capture.is_opened()? {
return Err(format!("Failed to open camera device {}", device_id).into());
}
Ok(Self {
capture,
frame_count: 0,
})
}
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let capture = videoio::VideoCapture::from_file(path, videoio::CAP_ANY)?;
if !capture.is_opened()? {
return Err(format!("Failed to open video file {}", path).into());
}
Ok(Self {
capture,
frame_count: 0,
})
}
pub fn capture_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>> {
let mut frame = core::Mat::default();
if self.capture.read(&mut frame)? {
if frame.empty() {
return Err("Captured frame is empty".into());
}
let timestamp = Utc::now();
let index = self.frame_count;
self.frame_count += 1;
Ok(Frame::new(frame, timestamp, index))
} else {
Err("Failed to capture frame".into())
}
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("*** Watermark Overlay Demo ***");
println!("This demo adds a watermark overlay to frames from a camera or video file");
println!("Press 'q' to quit");
println!("Press 'p' to change watermark position");
println!("Press 'c' to change GPS coordinates");
println!("Press 't' to change temperature and humidity");
println!();
println!("Choose input source:");
println!("1. Camera device");
println!("2. Video file");
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let mut camera_demo = match choice.trim() {
"1" => {
println!("Enter camera device ID (default is 0):");
let mut device_id = String::new();
io::stdin().read_line(&mut device_id)?;
let device_id = device_id.trim().parse::<i32>().unwrap_or(0);
println!("Opening camera device {}", device_id);
camera::CameraDemo::new(device_id)?
},
"2" => {
println!("Enter video file path:");
let mut file_path = String::new();
io::stdin().read_line(&mut file_path)?;
let file_path = file_path.trim();
println!("Opening video file: {}", file_path);
camera::CameraDemo::from_file(file_path)?
},
_ => {
println!("Invalid choice, using default camera (device 0)");
camera::CameraDemo::new(0)?
}
};
// Create watermark demo
let mut watermark_options = overlay::WatermarkOptions::default();
let mut watermark_demo = overlay::WatermarkDemo::new(watermark_options.clone())?;
highgui::named_window("Watermark Overlay Demo", highgui::WINDOW_NORMAL)?;
loop {
match camera_demo.capture_frame() {
Ok(frame) => {
// Apply watermark to the frame
let mut display_frame = frame.image.clone();
if let Err(e) = watermark_demo.apply(&mut display_frame, frame.timestamp) {
println!("Error applying watermark: {}", e);
}
// Display the frame
highgui::imshow("Watermark Overlay Demo", &display_frame)?;
// Handle key presses
let key = highgui::wait_key(30)?;
match key as u8 as char {
'q' => break, // Quit
'p' => {
// Cycle through positions
watermark_options.position = match watermark_options.position {
overlay::WatermarkPosition::TopLeft => overlay::WatermarkPosition::TopRight,
overlay::WatermarkPosition::TopRight => overlay::WatermarkPosition::BottomRight,
overlay::WatermarkPosition::BottomRight => overlay::WatermarkPosition::BottomLeft,
overlay::WatermarkPosition::BottomLeft => overlay::WatermarkPosition::TopLeft,
_ => overlay::WatermarkPosition::TopLeft,
};
watermark_demo = overlay::WatermarkDemo::new(watermark_options.clone())?;
println!("Changed watermark position: {:?}", watermark_options.position);
},
'c' => {
// Change to some predefined GPS locations
static LOCATIONS: [(f64, f64, f64, &str); 4] = [
(34.0522, -118.2437, 85.0, "Los Angeles"),
(40.7128, -74.0060, 10.0, "New York"),
(51.5074, -0.1278, 20.0, "London"),
(35.6762, 139.6503, 40.0, "Tokyo"),
];
static mut LOCATION_INDEX: usize = 0;
unsafe {
LOCATION_INDEX = (LOCATION_INDEX + 1) % LOCATIONS.len();
let (lat, lon, alt, name) = LOCATIONS[LOCATION_INDEX];
watermark_demo.set_gps(lat, lon, alt);
println!("Changed location to {}: Lat={}, Lon={}, Alt={}m", name, lat, lon, alt);
}
},
't' => {
// Cycle through different weather conditions
static CONDITIONS: [(f32, f32, &str); 4] = [
(25.0, 60.0, "Warm and Humid"),
(32.0, 80.0, "Hot and Very Humid"),
(15.0, 40.0, "Cool and Dry"),
(5.0, 30.0, "Cold and Dry"),
];
static mut CONDITION_INDEX: usize = 0;
unsafe {
CONDITION_INDEX = (CONDITION_INDEX + 1) % CONDITIONS.len();
let (temp, humidity, desc) = CONDITIONS[CONDITION_INDEX];
watermark_demo.set_weather(temp, humidity);
println!("Changed weather to {}: Temp={} deg C, Humidity={}%", desc, temp, humidity);
}
},
_ => {}
}
},
Err(e) => {
println!("Error capturing frame: {}", e);
break;
}
}
}
highgui::destroy_all_windows()?;
Ok(())
}